C#异步操作(APM)
上一篇文章:多线程和异步的区别
同步:执行一个耗时的操作时,主线程就会一直等待,直到该操作执行完成,我们大部分程序都是同步操作。例如,你给朋友发了一个消息:一起玩王者农药,但是此时你朋友正在游戏中,你为了他能带你躺赢,你就一直等待他把这局打完,然后一起开黑。
异步:执行一个耗时的操作时,主线程不会等待,会继续执行下面的操作,当耗时的操作完成时,它会通过回调的方式告诉主线程我执行完了,然后主程序在回调函数里继续后续的操作。例如,你给朋友发了一个消息:一起玩王者农药,但是此时你朋友正在游戏中,虽然他是大神,你为了证明自己不是坑,不用他带依然可以上分,果断单独开了一局,等你朋友虐菜完成,通知你说,我打完了,要不要我带你飞。
IO操作
1 2 3 4 5 6 7 8 9 |
static void Main(string[] args) { Console.WriteLine("Start"); using (FileStream fs = new FileStream("Data.dxf", FileMode.OpenOrCreate, FileAccess.Read, FileShare.None)) { fs.Read(new byte[1024], 0, 1024); } Console.WriteLine("End"); } |
IO流操作使我们程序开发中很常见的与硬盘的交互方式,如: 上面代码中Data.dxf文件很大,fs.Read读取过程中,我们只能一直等待Read方法执行完,才能打印“End”。程序主界面一定会处于卡死状态,用户体验较差,此时我们就需要用到异步方式去读取文件流。如何去执行异步操作呢?
BeginXXX / EndXXX
1 2 3 4 5 6 7 8 9 |
static void Main(string[] args) { Console.WriteLine("Start"); using (FileStream fs = new FileStream("Data.dxf", FileMode.OpenOrCreate, FileAccess.Read, FileShare.None)) { fs.BeginRead(new byte[1024], 0, 1024, null, null);//FileStream提供了异步调用的方法 } Console.WriteLine("End"); } |
.Net早期,采用BeginXXX/EndXXX方式实现异步,你在使用这些方法的时候,你就在使用异步编程模型来编写程序。异步编写模型是一种模式,该模式允许用更少的线程去做更多的操作,.NET Framework很多类也实现了该模式,例如上面的FileStream(实现返回类型为IAsyncResult接口的BeginXXX和EndXXX方法),委托也定义了BeginInvoke和EndInvoke的方法。经常看到和使用类似BeginXXX和EndXXX方法,即是采用APM即异步编程模型(Asynchronous Programming Model)
我们先抛开IO的例子,先看一个委托的同步方法实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
static void Main(string[] args) { Action<string> action = DoSomething; //实例化委托 action.Invoke("DoSomething Start"); //同步执行 Console.WriteLine("go on"); } static void DoSomething(string s) { Console.WriteLine(s); Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread == true ? "pool thread" : "not pool thread"); //是否是线程池 Thread.Sleep(8000); Console.WriteLine("DoSomething End"); } 执行结果: DoSomething Start not pool thread DoSomething End go on |
调用Invoke方法,主线程就会等待DoSomething执行完,再继续往下执行。
现在换成BeginInvoke再打印一下结果
1 2 3 4 5 6 7 8 9 10 11 12 |
static void Main(string[] args) { Action<string> action = DoSomething; //实例化委托 action.BeginInvoke("DoSomething Start", null, null); //异步执行 Console.WriteLine("go on"); } 输出结果: go on DoSomething Start pool thread DoSomething End |
调用BeginInvoke后,主线程根本不会等待DoSomething执行完成,就打印了“go on!”。还有一点,DoSomething方法里面的线程是线程池提供的线程,也就是BeginInvoke的异步操作是基于线程池实现的。
AsyncCallback、IAsyncResult
在上面代码中我们可以看到BeginInvoke方法里面有两个参数为null,这两个参数难道没作用吗?非也。看一下BeginInvoke的方法参数构造。
第一个参数:DoSomething方法中的参数,如果DoSomething方法没有参数,则BeginInvoke就只有后面两个参数
第二个参数:AsyncCallback callback,看一下AsyncCallback这是个什么东东?
1 2 3 4 5 6 7 8 9 10 11 12 |
namespace System { // 摘要: // 引用在相应异步操作完成时调用的方法。 // // 参数: // ar: // 异步操作的结果。 [Serializable] [ComVisible(true)] public delegate void AsyncCallback(IAsyncResult ar); } |
哦,原来是一个委托,没有返回值,接收一个IAsyncResult类型的参数。
1 |
AsyncCallback callback = ac => Console.WriteLine("This is Callback {0}", Thread.CurrentThread.ManagedThreadId); |
上面用Lambda表达式实例化一个AsyncCallback,如果对lambda表达式不是很懂,请看这篇文章:Lambda表达式和Linq
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static void Main(string[] args) { Action<string> action = DoSomething; AsyncCallback callback = ac => Console.WriteLine("This is Callback {0}", Thread.CurrentThread.ManagedThreadId); action.BeginInvoke("DoSomething Start", callback, null); Console.WriteLine("go on"); } 执行结果: go on DoSomething Start pool thread DoSomething End This is Callback 3 |
从结果中我们看到:最后一句是在异步完成之后打印的,其实这就是我们异步回调后的操作。你可能会问这个ac代表了什么,从委托AsyncCallback的参数中我们可以推断出ac就是IAsyncResult类型的实例。但是这个IAsyncResult的实例从哪里来的呢?也没见传进来就执行通过了。
1 2 3 4 5 6 7 8 9 10 |
static void Main(string[] args) { Action<string> action = DoSomething; IAsyncResult iAsyncResult = null; //定义 AsyncCallback callback = ac => { Console.WriteLine(string.ReferenceEquals(ac, iAsyncResult)); //判断ac和iAsyncResult是否相等 }; iAsyncResult = action.BeginInvoke("DoSomething Start", callback, null); } |
BeginInvoke方法的返回值是iAsyncResult,上面代码输出结果为”true”,所以ac就是BeginInvoke的返回值iAsyncResult,AsyncCallback这个委托就是执行异步后续的操作。
再来看一下IAsyncResult接口构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public interface IAsyncResult { // // 摘要: // 获取用户定义的对象,它限定或包含关于异步操作的信息。 object AsyncState { get; } // // 摘要: // 获取用于等待异步操作完成的 System.Threading.WaitHandle。 WaitHandle AsyncWaitHandle { get; } // // 摘要: // 获取一个值,该值指示异步操作是否同步完成。 bool CompletedSynchronously { get; } // // 摘要: // 获取一个值,该值指示异步操作是否已完成。 bool IsCompleted { get; } } |
IAsyncResult存储了异步方法的相关状态信息,其中AsyncState属性存储了BeginInvoke方面里面第三个参数。下面我们自己写一个简单的两个数字相加异步调用的例子
自定义IAsyncResult
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public class WydAsyncResult : IAsyncResult { private readonly object obj_State; private readonly AsyncCallback async_callback; private ManualResetEvent m_AsyncWaitHandle; //信号量 public WydAsyncResult(int num1, int num2, AsyncCallback callback, object objState) { this.obj_State = objState; this.async_callback = callback; m_AsyncWaitHandle = new ManualResetEvent(false);//非终止状态 } public void SetComplete() //异步完成 { IsCompleted = true; if (m_AsyncWaitHandle != null) m_AsyncWaitHandle.Set();//终止 if (async_callback != null) async_callback(this);//执行回调 } public int Result //倆数值相加的结果 { get; set; } public object AsyncState { get { return obj_State; } } public WaitHandle AsyncWaitHandle { get { return m_AsyncWaitHandle; } } public bool CompletedSynchronously { get { return false; } } public bool IsCompleted { get; private set; } } |
调用类WydAsyncTask
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
public class WydAsyncTask { /// <summary> /// 开始异步方法 /// </summary> /// <param name="num1">数字1</param> /// <param name="num2">数字2</param> /// <param name="callback">AsyncCallback</param> /// <param name="state">要传入的参数</param> /// <returns></returns> public IAsyncResult BeginAsyncTask(int num1, int num2, AsyncCallback callback, object state) { WydAsyncResult mr = new WydAsyncResult(num1, num2, callback, state); ThreadPool.QueueUserWorkItem(t => { mr.Result = num1 + num2;//此处执行你的需求 mr.SetComplete();//告知异步完成 });//线程池 return mr; } /// <summary> /// 结束异步方法得到最终结果 /// </summary> /// <param name="asyncResult">IAsyncResult</param> /// <returns>返回二者之和</returns> public int EndAsyncTask(IAsyncResult asyncResult) { WydAsyncResult result = asyncResult as WydAsyncResult; if (!result.IsCompleted) result.AsyncWaitHandle.WaitOne(); //阻塞线程 return result.Result; } /// <summary> /// 同步方法 /// </summary> /// <param name="num1">数字1</param> /// <param name="num2">数字2</param> /// <returns>返回二者之和</returns> public int Task(int num1, int num2) { int result = num1 + num2; return result; } } |
Main方法调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static void Main(string[] args) { WydAsyncTask asyncTask = new WydAsyncTask(); int result =asyncTask.Task(2, 3);//同步调用 Console.WriteLine("同步Result=" + result.ToString()); AsyncCallback callback = ac => { Console.WriteLine(ac.AsyncState.ToString()); }; IAsyncResult iAsyncResult = asyncTask.BeginAsyncTask(2, 3, callback, "callbacking"); result = asyncTask.EndAsyncTask(iAsyncResult); Console.WriteLine("异步Result=" + result.ToString()); Console.ReadKey(); } 输出结果: 同步Result=5 异步Result=5 callbacking |
同步调用可以直接返回Int结果,异步调用就需要调用EndAsyncTask方法才能取得Int结果。回调函数里面ac.AsyncState的值就是传入的callbacking。
对于访问异步操作的结果,APM提供了四种方式供开发人员选择:
- 在调用BeginXxx方法的线程上调用EndXxx方法来得到异步操作的结果,但是这种方式会阻塞调用线程,知道操作完成之后调用线程才继续运行
- 查询IAsyncResult的AsyncWaitHandle属性,从而得到WaitHandle,然后再调用它的WaitOne方法来使一个线程阻塞并等待操作完成再调用EndXxx方法来获得操作的结果。
- 循环查询IAsyncResult的IsComplete属性,操作完成后再调用EndXxx方法来获得操作返回的结果。
- 使用 AsyncCallback委托来指定操作完成时要调用的方法,在操作完成后调用的方法中调用EndXxx操作来获得异步操作的结果。
第一种方式:上面Main方法里面的写法,在BeginXXX后面写EndXXX。
第二种方式:WaitOne()
1 2 3 |
IAsyncResult iAsyncResult = asyncTask.BeginAsyncTask(2, 3, callback, "callbacking"); iAsyncResult.AsyncWaitHandle.WaitOne();//阻塞线程 WaitAll WaitAny asyncTask.EndAsyncTask(iAsyncResult); |
第三种方式:轮询
1 2 3 4 5 6 |
IAsyncResult iAsyncResult = asyncTask.BeginAsyncTask(2, 3, callback, "callbacking"); while (!iAsyncResult.IsCompleted)//边等待 边操作 { //等待中操作,进度条之类的 } asyncTask.EndAsyncTask(iAsyncResult); |
第四种方式:回调函数中执行(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void Main(string[] args) { WydAsyncTask asyncTask = new WydAsyncTask(); IAsyncResult iAsyncResult = null; AsyncCallback callback = ac => { Console.WriteLine(ac.AsyncState.ToString()); int result = asyncTask.EndAsyncTask(iAsyncResult);//放到回调函数中 Console.WriteLine("异步Result=" + result.ToString()); }; iAsyncResult = asyncTask.BeginAsyncTask(2, 3, callback, "callbacking"); Console.ReadKey(); } |
看到这,文章最开始的FileStream类中的BeginRead应该知道怎么实现的了吧。
实例
做过Winform的猿们肯定遇到过一个问题:多线程访问Winform的控件报错的问题,下面还原一下报错场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
private void Form1_Load(object sender, EventArgs e) { Console.WriteLine("当前线程Id为:" + Thread.CurrentThread.ManagedThreadId); Action action = () => { Console.WriteLine("当前线程Id为:" + Thread.CurrentThread.ManagedThreadId); lblShow.Text = DateTime.Now.ToString(); }; action.BeginInvoke(null, null); } 打印结果: 当前线程Id为:1 当前线程Id为:3 |
窗体创建一个Label控件,在窗体Load方法里面通过委托的异步调用把当前时间赋值给Label控件,同时把当前线程唯一标识Id打印一下。调试运行一下,错误信息如下图:
错误信息:线程间操作无效,从不是创建控件”lblShow”的线程访问它。什么意思呢?
也就是说控件”lblShow”不允许不是创建它的线程访问,“lblShow”是主线程创建,Action委托通过线程池新开另一个线程去操作”lblShow”控件是不允许的。从上面的打印结果也能看出两者不是同一个线程。
如何去解决这个问题呢?
Winform程序遇到耗时任务时,让用户看着卡死的界面等待耗时任务不可取,我们可以通过另开一个线程去执行这个耗时任务,把主线程从耗时任务中解脱出来。
但是为了保证线程安全,框架不允许你新开的线程直接去操作控件,既然控件只能有主线程访问,所以只有一条路,”lblShow”控件的赋值重任还得落在主线程身上。
那还得从”lblShow”控件入手,看看微软给我们提供了什么?查看Control基类发现,每一个控件都有一个Invoke方法。方法构造如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
// // 摘要: // 在拥有此控件的基础窗口句柄的线程上执行指定的委托。 // // 参数: // method: // 包含要在控件的线程上下文中调用的方法的委托。 // // 返回结果: // 正在被调用的委托的返回值,或者如果委托没有返回值,则为 null。 [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public object Invoke(Delegate method); |
此方法参数需要传入一个委托,那我们现在写写看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private void Form1_Load(object sender, EventArgs e) { Console.WriteLine("当前线程Id为:" + Thread.CurrentThread.ManagedThreadId); Action action = () => { Console.WriteLine("当前线程Id为:" + Thread.CurrentThread.ManagedThreadId); Action actionControl = UpdateControl; lblShow.Invoke(actionControl); //控件的Invoke方法 }; action.BeginInvoke(null, null); } private void UpdateControl() { Console.WriteLine("当前线程Id为:" + Thread.CurrentThread.ManagedThreadId); lblShow.Text = DateTime.Now.ToString(); } 打印结果为: 当前线程Id为:1 当前线程Id为:3 当前线程Id为:1 |
此时再去调试发现,上面的错误没有了,为什么呢?看一下打印结果,在UpdateControl方法里面的线程Id不就是主线程的Id吗,主线程(创建控件线程)去操作”lblShow”控件当然不会报上面的跨线程访问控件的错误信息了。所以可以看出调用控件的Invoke方法,作为参数的委托内部方法执行是由主线程来完成的。
上面的例子只是简单的打印下当前时间,并没有体会到耗时。下面我们加一个while(true)循环,让”lblShow”控件的时间动起来,这个循环代码写到哪里呢?写到UpdateControl里面?稍微想一下就可以否决了,这个方法里面是主线程在操作,里面加个死循环,那界面永远处于卡死状态。所以我们把while(true)语句加在新开辟的线程里面。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
private void Form1_Load(object sender, EventArgs e) { Action action = () => { while (true) { Action actionControl = UpdateControl; lblShow.Invoke(actionControl); Thread.Sleep(1000); } }; action.BeginInvoke(null, null); } private void UpdateControl() { lblShow.Text = DateTime.Now.ToString(); } |
再贴出一个Thread写法(Thread后续文章会讲)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private void Form1_Load(object sender, EventArgs e) { Thread thread = new Thread(new ThreadStart(() => { while (true) { Action action = UpdateControl; lblShow.BeginInvoke(action); Thread.Sleep(1000); } })); thread.IsBackground = true; thread.Start(); } private void UpdateControl() { lblShow.Text = DateTime.Now.ToString(); } |
此时我们的时间就可以愉快的跑起来了,即不卡界面,也不存在跨线程操作控件的问题。
希望对大家有一点帮助,文中如有错误,烦劳您不吝指出,谢谢!
如果转载,请给出原文链接,谢谢!