委托的异步性与方法回调
知识回顾:
首先声明一个委托如下:
// 这个委托指向任意输入两个整数并返回一个整数的方法. public delegate int CalculateDelegate(int x,int y);
查看IL代码:
由图可以看出,C#编译器处理委托类型时,自动生成派生于 System.MulticastDelegate 的 sealed 类。并定义了三个方法,其中 Invoke() 方法被用来同步的方式调用方法;BeginInvoke() 和 EndInvoke() 方法被用来异步调用方法。
以下是生成的代码:
sealed class CalculateDelegate:System.MulticastDelegate { public CalculateDelegate(object @object, IntPtr method); public virtual IAsyncResult BeginInvoke(int x, int y, AsyncCallback callback, object @object); public virtual int EndInvoke(IAsyncResult result); public virtual int Invoke(int x, int y); }
构造函数的名称匹配委托名,参数格式固定;
BeginInvoke() 方法中后两个参数固定,前面的参数匹配委托定义的参数;
EndInvoke() 方法和 Invoke() 方法的返回值类型匹配委托的返回值类型;
Invoke() 方法中的参数列表匹配委托的参数列表;
其余代码为固定格式。
特殊的:
当委托指向包含任意数量 out 或 ref 参数(以及用 params 关键字标记的数组参数)的方法。如下:
public delegate int CalculateDelegate(out bool a,ref bool b,int x,int y);
查看生成的代码:
sealed class CalculateDelegate : System.MulticastDelegate { public CalculateDelegate(object @object, IntPtr method); public virtual IAsyncResult BeginInvoke(out bool a, ref bool b, int x, int y, AsyncCallback callback, object @object); // 只有 EndInvoke() 方法的参数发生改变 public virtual int EndInvoke(out bool a, ref bool b, IAsyncResult result); public virtual int Invoke(out bool a, ref bool b, int x, int y); }
可以发现,只有当包含 out 、ref 或 params标记时,EndInvoke() 方法的参数才发生了改变。
在演示委托的异步性之前,先了解一下委托的同步模式。
同步模式:
代码如下:
class Program { // 这个委托指向任意输入两个整数并返回一个整数的方法. public delegate int CalculateDelegate(int x,int y); // 定义与委托匹配的方法 static int Add(int x, int y) { // 输出正在执行 Add() 方法的线程ID Console.WriteLine("Add() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); // 为了更容易观察,模拟一个耗时的操作 // 休眠5秒 Thread.Sleep(TimeSpan.FromSeconds(5)); return x + y; } static void Main(string[] args) { // 输出正在执行Main()方法的线程ID Console.WriteLine("Main() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); // 在同步模式下调用Add()方法 CalculateDelegate calculate = new CalculateDelegate(Add); // 也可写成calculate(12,12); int answer=calculate.Invoke(12,12); Console.WriteLine("Doing more work in Main()..."); Console.WriteLine("12 + 12 = {0}",answer); Console.ReadKey(); } }
输出:
Main() invoked on thread 9 Add() invoked on thread 9 Doing more work in Main()... 12 + 12 = 24
当程序输出第二行之后,阻塞了大约5秒钟的时间才继续执行后面的代码,因此 Main() 方法等到 Add() 方法执行结束才输出结果,而且有线程ID可以,它们是在同一个线程上执行的。
可见,当我们在执行一个耗时的操作时,如:下载或数据库查询时,应用程序将被挂起很长时间,特别在编写 WinForm 窗体应用程序时,更为常见。
如何让委托在单独的线程上能够调用方法,以便模拟多个“同时”运行的任务?虽然使用多线程也可以实现,但是.Net Framework中的每个委托(包括自定义委托)都被自动赋予同步或异步访问方法的能力,可以在不用收工创建与管理一个 Thread 对象而直接调用另一个辅助线程上的方法,这大大简化了编程工作。
异步模式:
更改上面的 Main() 方法,如下:
static void Main(string[] args) { // 输出正在执行Main()方法的线程ID Console.WriteLine("Main() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); // 在次线程中调用Add()方法 CalculateDelegate calculate = new CalculateDelegate(Add); IAsyncResult result = calculate.BeginInvoke(12,12,null,null); // 在主线程中做别的事情 Console.WriteLine("Doing more work in Main()..."); // 异步调用结束后获取Add()方法的结果 int answer = calculate.EndInvoke(result); Console.WriteLine("12 + 12 = {0}",answer); Console.ReadKey(); }
输出:
Main() invoked on thread 9 Doing more work in Main()... Add() invoked on thread 10 12 + 12 = 24
当程序运行时 "Doing more work in Main()..." 被立即输出,而 Add() 方法则在次线程继续执行,由线程ID不同也可判断出。
这里的 calculate调用了BeginInvoke()方法,它的后两个参数稍后解释,前两个参数则是定义委托的参数列表。返回一个IAsyncResult类型的对象 result,并在下面调用 EndInvoke() 方法中传入了 result 对象(因为此方法要求传入 IAsyncResult 类型的参数),EndInvoke() 方法的返回值就是方法Add的返回值。
说明:如果异步调用一个无返回值的方法,仅调用 BeginInvoke() 就可以了。不需要缓存 IAsyncResult 兼容对象,也不需要调用 EndInvoke() 方法来获取返回值。
同步调用线程:
对上面的代码进行分析,Add() 方法所花费的时间显然低于5秒,也就是说,在当 "Do more work in Main()..."这句话被输出之后,主线程一直被阻塞,等待次线程执行结束并返回结果。如何知道次线程是否执行结束,以便于在主线程上可以做别的事情?
IAsyncResult 接口提供了 IsCompleted 属性,可以用来判断异步调用是否真正完成。更改 Main() 方法如下:
static void Main(string[] args) { // 输出正在执行Main()方法的线程ID Console.WriteLine("Main() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); // 在次线程中调用Add()方法 CalculateDelegate calculate = new CalculateDelegate(Add); IAsyncResult result = calculate.BeginInvoke(12,12,null,null); while (!result.IsCompleted) { // 在主线程中做别的事情 Console.WriteLine("Doing more work in Main()..."); Thread.Sleep(TimeSpan.FromSeconds(1)); } // 异步调用结束后获取Add()方法的结果 int answer = calculate.EndInvoke(result); Console.WriteLine("12 + 12 = {0}",answer); Console.ReadKey(); }
输出:
Main() invoked on thread 10 Doing more work in Main()... Add() invoked on thread 11 Doing more work in Main()... Doing more work in Main()... Doing more work in Main()... Doing more work in Main()... Doing more work in Main()... 12 + 12 = 24
在次线程完成之前,循环将不断的执行,一旦次线程执行完成,则执行循环下面的代码(为了避免 Do more work in Main()...被输出几百次,每次循环时让线程休眠一秒钟)。
除了使用 IsCompleted 属性之外,IAsyncResult 接口还提供了 AsyncWaitHandle 属性以实现更更加灵活的阻塞逻辑控制。这个属性返回一个 WaitHandle 类型的实例,该实例公开了一个名为 WaitOne() 方法。使用 WaitHandle.WaitOne() 的好处是可以指定最长的等待时间。如果超时,WaitOne() 返回 false。
考虑如下代码:
while (!result.AsyncWaitHandle.WaitOne(1000,true)) { // 在主线程中做别的事情 Console.WriteLine("Doing more work in Main()..."); }
这里省去了 Thread.Sleep(TimeSpan.FromSeconds(1)); 这行代码。
虽然 IAsyncResult 的这些属性提供了同步调用线程的方式,但这并不是最高效的方式。因此,IsCompleted 属性总是不断的询问次线程是否完成,能不能在次线程执行完成后主动通知调用线程?这就是委托的回调。
我们需要在调用 BeginInvoke() 时提供一个 System.AsyncCallback 委托的实例作为参数,这个参数默认值为null,这样当次线程完成后便会自动调用 (AsyncCallback对象)指定的方法。
AsyncCallback 委托定义如下:
public delegate void AsyncCallback(IAsyncResult ar);
首先我们来添加一个匹配委托的方法:
static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoke on thread {0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Your addition is complete."); }
接着更改 Main() 方法中的代码:
static void Main(string[] args) { // 输出正在执行Main()方法的线程ID Console.WriteLine("Main() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); // 在次线程中调用Add()方法 CalculateDelegate calculate = new CalculateDelegate(Add); AsyncCallback callBack = new AsyncCallback(AddComplete); IAsyncResult result = calculate.BeginInvoke(12,12,callBack,null); // 这里没有获取方法的计算结果 // 思考如何将显示任务交给 AddComplete() 方法 Console.ReadKey(); }
输出:
Main() invoked on thread 10 Add() invoked on thread 11 AddComplete() invoke on thread 11 Your addition is complete.
由输出结果可知,次线程完成后自动调用了 AddComplete() 方法。
上面的代码中,Main() 方法没有缓存 BeginInvoke() 返回的 IAsyncResult 类型,并且不再调用 EndInvoke() 。如何 AddComplete() 中自动完成这些工作呢?
使用定义在 System.Runtime.Remoting.Messaging 命名空间下的 AsyncResult 类,(注意,前面没有 ‘I’ )。该类的静态属性 AsyncDelegate 返回了别处创建的原始异步委托的引用。因此如果想获取对分配在 Main() 中的 CalculateDelegate 委托对象的引用,只需把由 AsyncDelegate 属性返回的 System.Object 类型转换成 CalculateDelegate 类型就可以了。更改 AddComplete()代码如下:
static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoke on thread {0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Your addition is complete."); // 获取并显示计算结果 AsyncResult ar = (AsyncResult)itfAR; CalculateDelegate cal = (CalculateDelegate)ar.AsyncDelegate; Console.WriteLine("12 + 12 = {0}",cal.EndInvoke(itfAR)); }
此时已经能够在 AddComplete() 方法中实现我们想要的结果。
现在我们需要关注的地方是 BeginInvoke() 方法的最后一个参数,该参数允许从主线程传递额外的状态信息给回调方法。这个参数的类型是 System.Object ,因此我们可以传入任何回调方法所希望的类型的数据。接下来我们传入一个自定义的文本消息作为示例:
更改 Main() 方法:
static void Main(string[] args) { // 输出正在执行Main()方法的线程ID Console.WriteLine("Main() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); // 在次线程中调用Add()方法 CalculateDelegate calculate = new CalculateDelegate(Add); AsyncCallback callBack = new AsyncCallback(AddComplete); // 传入自定义的文本消息 IAsyncResult result = calculate.BeginInvoke(12,12,callBack,"Main() thanks for adding these numbers."); Console.ReadKey(); }
更改 AddComplete() 方法:
static void AddComplete(IAsyncResult itfAR) { Console.WriteLine("AddComplete() invoke on thread {0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("Your addition is complete."); // 获取并显示计算结果 AsyncResult ar = (AsyncResult)itfAR; CalculateDelegate cal = (CalculateDelegate)ar.AsyncDelegate; Console.WriteLine("12 + 12 = {0}",cal.EndInvoke(itfAR)); // 获取自定义的文本消息 string str = (string)itfAR.AsyncState; Console.WriteLine(str); }
输出:
Main() invoked on thread 9 Add() invoked on thread 10 AddComplete() invoke on thread 10 Your addition is complete. 12 + 12 = 24 Main() thanks for adding these numbers.
附:
使用匿名方法实现,修改 Main() 方法:
static void Main(string[] args) { // 输出正在执行Main()方法的线程ID Console.WriteLine("Main() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); // 在次线程中调用Add()方法 CalculateDelegate calculate = delegate(int x, int y) { Console.WriteLine("Add() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(TimeSpan.FromSeconds(5)); return x + y; }; AsyncCallback callBack = new AsyncCallback(AddComplete); // 传入自定义的文本消息 IAsyncResult result = calculate.BeginInvoke(12,12,callBack,"Main() thanks for adding these numbers."); Console.ReadKey(); }
使用 Lambda 表达式实现,更改 Main() 方法:
// 在次线程中调用Add()方法 CalculateDelegate calculate = (x,y)=> { Console.WriteLine("Add() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId); Thread.Sleep(TimeSpan.FromSeconds(5)); return x + y; };
同样,可以更改 AsyncCallback callBack = new AsyncCallback(AddComplete); 实现以上两种形式。