c# 基于委托的异步编程模型(APM)测试用例
很多时候,我们需要程序在执行某个操作完成时,我们能够知道,以便进行下一步操作。
但是在使用原生线程或者线程池进行异步编程,没有一个内建的机制让你知道操作什么时候完成,为了克服这些限制,基于委托的异步编程模型应运而生。
通过定义回调函数能够实现异步编程,委托是一个工具,类似语c++的函数指针,当我们在使用委托时。可以传入一个符合其定义的方法。从编译器的层面看,定义一个委托,相当于定义了一个类,该类拥有一个委托链,还有三个方法,Invoke用于同步调用,而BeginInvoke和EndInvoke则是用于异步调用。
先来对这三个方法进行说明:
Invoke方法的输入输出和委托本身相同。注意:Invoke方法的主要功能就是帮助你在UI线程上调用委托所指定的方法。Invoke方法首先检查发出调用的线程(即当前线程)是不是UI线程,如果是,直接执行委托指向的方法,如果不是,它将切换到UI线程,然后执行委托指向的方法。不管当前线程是不是UI线程,Invoke都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。
BeginInvoke的输出是IAsyncResult,输入除了委托本身的输入,还包括一个回调函数AsyncCallBack,以及包括了一个object的类型参数,允许我们向异步委托传递任何类型的信息。如果把一个回调函数传入BeginInvoke,它会在委托运行完成后自动执行。
EndInvoke方法的输入总是IAsyncResult,输出则是和委托本身的输出相同。如果调用EndInvoke时,IAsyncResult对象表示的异步操作还未完成,则EndInvoke将在异步操作未完成之前阻塞调用线程(非常重要)。
如何理解这些方法的输入输出?我们可以把BeginInvoke 作为调用的开始,当调用完时,我们希望有一个回调函数,可以在希望的时候调用,从而让异步方法主动通知我们自己以及完成。so BeginInvoke的输入除了委托本身的输入外,还需要一个回到函数AsyncCallBack(当然也可以不写这个函数,传入null,这将失去主动通知的好处)。另外一个object类型的state参数,允许我们向异步委托传输任意类型的信息。这个参数,我们能在回调函数中获取到,这更加方便实现各种业务逻辑。
示例1:获取异步委托的执行结果
我们先将回调函数和状态设为null,通过EndInvoke异步委托获取目标函数的返回值,和Threading的方法相比,我们可以在委托目标的函数中设定返回值的类型,使获取结果容易了很多,但是这样会造成阻塞。因为EndInvoke强制获取结果,所以结果在没有计算出来之前,代码是无法向前进行的。这和Threading加轮询的方法类型,只是现在线程由线程池管理。
class Program { public delegate bool IsPrimeSlowDelegate(int number); public static bool IsPrimeSlow(int number) { bool b = false; if (number <= 0) { throw new Exception("参数必须大于0"); } if (number == 1) { throw new Exception("1既不是质数也不是合数"); } for (int i = 2; i <= number; i++) { Thread.Sleep(100); if (number % i == 0) { b = false; } else { b = true; } } return b; } static void Main(string[] args) { IsPrimeSlowDelegate slowDelegate = new IsPrimeSlowDelegate(IsPrimeSlow); Console.WriteLine("开始执行:" + DateTime.Now.ToString()); var ar = slowDelegate.BeginInvoke(120,null,null); var er = slowDelegate.EndInvoke(ar); Console.WriteLine("结束执行:" + DateTime.Now.ToString()); Console.ReadKey(); } }
执行结果:(在执行时,EndInvoke一直在等待计算结果,阻塞了主线程12秒,实际上和同步调用无异,因此,我们应该使用回调函数)
BeginInvoke和EndInvoke是由一个IAsyncResult接口对象联系在一起
System.IAsyncResult接口包括:
1、一个object类型的AsyncState,存储主线程传来的的信息。
2、一个布尔类型,IsCompleted,当其为真,异步委托执行完毕。
3、一个类型为WaitHandle的属性AsyncWaitHandle.WaitHandle有个方法WaitOne,可以指定最长等待时间。
示例:
static void Main(string[] args) { IsPrimeSlowDelegate slowDelegate = new IsPrimeSlowDelegate(IsPrimeSlow); Console.WriteLine("开始执行:" + DateTime.Now.ToString()); IAsyncResult ar = slowDelegate.BeginInvoke(120,null,null); //设置线程最长等待时间为3秒 bool b = ar.AsyncWaitHandle.WaitOne(3000); if (b) { //三秒后,如果当前实例收到信号,则为 true;否则为 false。当没有收到信息,就跳过执行EndInvoke var er = slowDelegate.EndInvoke(ar); } Console.WriteLine("结束执行:" + DateTime.Now.ToString()); Console.ReadKey(); }
示例2:通过回调函数的方式获得异步委托的执行结果
回调函数的作用是当委托完成后,可以主动通过主线程自己已经完成。我们可以在BeginInvoke中定义回调函数,这将在委托完成后自动执行,也就是说,线程将执行完回调函数才回到线程池,而不是直接回到池子中。
回到函数的类型是AsyncCallBack,其也是一个委托,传入参数必须是IAsyncResult,而且没有返回值,那么我们怎么获取返回值呢?
此时,我们可以通过回调函数的传入参数IAsyncResult来做。我们把参数显示转换为AsyncResult的形式。AsyncResult实现了IAsyncResult,它的属性AsyncDelegate是object类型的,可以指向其他地方对象的引用。此时我们可以在回调函数中建立一个委托,令其类型和main中建立的委托相同,然后,将其赋值给AsyncDelegatee(需要转换)。这时,该委托就和Main中的那个毫无二致。现在我们可以调用EndInvoke获取结果了。而且这次调用不会阻塞,代码如下
static void Main(string[] args) { IsPrimeSlowDelegate slowDelegate = new IsPrimeSlowDelegate(IsPrimeSlow); var callBack = new AsyncCallback(CallBack); Console.WriteLine("开始执行:" + DateTime.Now.ToString()); slowDelegate.BeginInvoke(123,cancelltion.Token,callBack,"我是最后一个参数"); Console.WriteLine("结束执行:" + DateTime.Now.ToString()); Console.Read(); } public delegate bool IsPrimeSlowDelegate(int number); public static bool IsPrimeSlow(int number) { bool b = false; if (number <= 0 ) { throw new Exception("参数必须大于0"); } if (number == 1) { throw new Exception("1既不是质数也不是合数"); } for (int i = 2; i <= number;i++) { Thread.Sleep(100); if (number%i == 0) { b = false; } else { b = true; } } return b; } /// <summary> /// 回调函数 /// </summary> /// <returns></returns> public static void CallBack(IAsyncResult iar) { var ar = (AsyncResult)iar; var br = (IsPrimeSlowDelegate)ar.AsyncDelegate; try { var re = br.EndInvoke(iar); Console.WriteLine(re.ToString()); } catch (Exception exp) { Console.WriteLine(exp.Message); } }
主线程立即执行完成,回调函数在计算完成后自动调用。
回调函数的参数对象:
这就是真正的异步了,主线程拍完任务后还可以做其他事,而子线程在任务完成后执行回调函数。
主线程还可以向子线程传输任何类型的自定义数据,这通过BeginInvoke的最后一个参数实现。由于它是object类型,所以任何类型都可以传输。在子线程中,我们通过调用IAsyncResult的AsyncState属性获取主线程传来的数据。该示例中,红圈即为传递的参数
示例3:使用线程统一取消模型进行取消
使用委托的异步编程模型也可以使用线程统一取消模型进行取消。首先,我们需要为为委托和委托目标方法加入CancellationToken输入参数(必须修改委托定义才行,因为BeginInvoke不支持传入CancellationToken)。
然后在委托目标方法中,调用ThrowIfCancellationRequested(在循环中,保持一直监听),最后,需要在回到函数中加入try-catch,因为BeginInvoke不抛OperationCanceledException异常,只有EndInvoke才会。为了不阻塞主线程,回调函数获取结果的EndInvoke是唯一 的选择。代码如下
static void Main(string[] args) { //创建取消多线程的对象 CancellationTokenSource cancelltion = new CancellationTokenSource(); IsPrimeSlowDelegate slowDelegate = new IsPrimeSlowDelegate(IsPrimeSlow); var callBack = new AsyncCallback(CallBack); Console.WriteLine("开始执行:" + DateTime.Now.ToString()); slowDelegate.BeginInvoke(123,cancelltion.Token,callBack,"我是最后一个参数"); Console.WriteLine("结束执行:" + DateTime.Now.ToString()); Console.ReadKey(); Console.WriteLine("线程取消执行:"+DateTime.Now.ToString()); cancelltion.Cancel(); Console.Read(); } public delegate bool IsPrimeSlowDelegate(int number,CancellationToken token); public static bool IsPrimeSlow(int number,CancellationToken token) { bool b = false; if (number <= 0 ) { throw new Exception("参数必须大于0"); } if (number == 1) { throw new Exception("1既不是质数也不是合数"); } for (int i = 2; i <= number;i++) { Thread.Sleep(100); token.ThrowIfCancellationRequested(); if (number%i == 0) { b = false; } else { b = true; } } return b; } /// <summary> /// 回调函数 /// </summary> /// <returns></returns> public static void CallBack(IAsyncResult iar) { var ar = (AsyncResult)iar; var br = (IsPrimeSlowDelegate)ar.AsyncDelegate; try { var re = br.EndInvoke(iar); Console.WriteLine(re.ToString()); } catch (OperationCanceledException cancelExp) { Console.WriteLine("任务取消"); } catch (Exception exp) { Console.WriteLine(exp.Message); } }
取消执行回调函数中,EndInvoke抛出异常:
注意:用户在任务完成之前取消了,会抛出异常,任务完成后取消,并不会抛出异常
即使提前取消了任务,也会调用回调函数,抛出异常。
无论任务成功完成还是被取消,IAsyncResult对象的Is'Com'p'leted属性都是true;
如果委托传入小于0,和1的数据,也会抛出异常,为了捕获此类以及其他非取消线程异常。(使用catch(Exception exp)进行捕获)