前言
回调函数,其实就是 . Net 预置的一个委托,请看定义:
using System.Runtime.InteropServices; namespace System { // // 摘要: // 引用在相应异步操作完成时调用的方法。 // // 参数: // ar: // 异步操作的结果。 [ComVisible(true)] public delegate void AsyncCallback(IAsyncResult ar); }
符合这样形式的都可以算回调函数。
那回调函数存在的意义又在哪里呢?比如我们要上传一个比较大的文件,耗时可能比较久,又不能一直在这里等,就开一个异步线程去上传。上传完成后,又希望通知一下我们,这个 “通知” 任务就可以让回调函数去做。
那么问题来了,为什么不直接把通知任务放在异步线程的最后呢?因为有时候,这个通知任务的代码要保密,或者操作起来比较复杂,所以写异步操作的人只需要去调用就可以了。看代码:
using System; using System.Threading; namespace ConsoleApp2 { class Program { static void Main(string[] args) { Action<string> action = t => { Thread.Sleep(2000); Console.WriteLine("Action down,Args:{0},ThreadID:{1}", t , Thread.CurrentThread.ManagedThreadId); }; action.BeginInvoke("action arg", myCallBack, "callBack arg"); Console.WriteLine("Main down,ThreadID:{0}",Thread.CurrentThread.ManagedThreadId); Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId); Console.ReadKey(); } static void myCallBack(IAsyncResult asyncResult) { Console.WriteLine(asyncResult.IsCompleted); Thread.Sleep(200); Console.WriteLine("CallBack down,Args:{0},ThreadID:{1}", asyncResult.AsyncState, Thread.CurrentThread.ManagedThreadId); } } }
执行结果:
显然主线程很快就跑完了,异步 action 执行完后,回调了回调函数完成操作。可以看到,回调函数和 action 是在同一个线程里顺序运行的,注意 “顺序” 两个字,回调函数耗时 200 毫秒,而 action 耗时 3000毫秒,但是依然是 action 先打印出结果。
那么问题来了,既然是顺序执行,就算回调函数的代码要保密,或者操作起来比较复杂,那把回调函数封装成一个方法,放在 action 末尾不就好了,一样调用呀。
这个时候,就关联到异步阻塞的问题了。请看代码:
using System; using System.Threading; namespace ConsoleApp2 { class Program { static void Main(string[] args) { Action<string> action = t => { Thread.Sleep(2000); Console.WriteLine("Action down,Args:{0},ThreadID:{1}", t, Thread.CurrentThread.ManagedThreadId); }; IAsyncResult asyncResult = action.BeginInvoke("action arg", myCallBack, "callBack arg"); //1,阻止当前线程,直到异步 action 结束 action.EndInvoke(asyncResult); //2,阻止当前线程,直到异步 action 结束。 注意,不阻塞回调哦 asyncResult.AsyncWaitHandle.WaitOne(); Console.WriteLine("Main down,ThreadID:{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("{0},ThreadID:{1}", asyncResult.IsCompleted, Thread.CurrentThread.ManagedThreadId); Console.ReadKey(); } static void myCallBack(IAsyncResult asyncResult) { Console.WriteLine(asyncResult.IsCompleted); Thread.Sleep(100); Console.WriteLine("CallBack down,Args:{0},ThreadID:{1}", asyncResult.AsyncState, Thread.CurrentThread.ManagedThreadId); } } }
执行结果:
执行结果与刚才不同了,主线程被阻塞了,直到 action 异步完成,主线程才接着往下走。
这个时候注意,阻塞并没有阻塞回调函数,这个很关键。这正是很多时候需要的,在确保部分任务完成之后再执行接下来得任务,并且不阻塞异步线程。
上面 1、2 都可以阻塞异步线程,效果一样。但是可以给 WaitOne() 赋予参数,使用其他有参重载,实现阻塞一段时间而不是一直阻塞。
后记
有趣的是,在 .Net 预定义中发现了 AsyncCallback,却并没有发现 SyncCallback ,原来回调函数一定是异步线程才能操作的呀。
想想也是,都同步了,还谈什么多线程,谈什么阻塞,谈什么女朋友…