1.5 回调机制
1.5.1 回调机制的概念
软件模块之间总是存在着一定的接口,从调用方式上,可以把它们分为三类:同步调用、回调和异步调用。同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回。回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;异步调用是一种类似消息或事件的机制,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知,因此可知回调机制是实现异步调用的基础。
图1-15是一次异步调用的结构图。
图1-15异步调用的结构图
模块A对模块B进行同步调用,调用后立即返回,待模块B中的方法执行完毕之后,通过回调机制(促发模块A中声明的回调函数)通知模块A调用完毕,并可以返回调用的结果。
客户和服务的交互除了同步方式以外,还需要具备一定的异步通知机制,让服务方(或接口提供方)在某些情况下能够主动通知客户,而回调是实现异步的一个最简捷的途径。
在面向对象的语言中,回调是通过接口或抽象类来实现的,我们把实现这种接口的类称为回调类,回调类的对象称为回调对象。C#是兼容了过程特性的对象语言,不仅提供了回调对象、回调方法等特性,也能兼容过程语言的回调函数机制。
Windows平台的消息机制也可以看做回调的一种应用,我们通过系统提供的接口注册消息处理函数(即回调函数),来实现接收、处理消息的目的。由于Windows平台的API是用C语言来构建的,我们可以认为它也是回调函数的一个特例。
1.5.2 回调方法实现的一般过程
在异步调用中一般使用回调机制,回调是用委托来实现的,实现过程一般有如下几步。
1.BeginInvoke方法可启动异步调用
BeginInvoke方法需要异步执行的方法具有相同的参数,另外它还有两个可选参数。第一个可选参数是一个AsyncCallback委托,该委托引用在异步调用完成时要调用的方法。第二个可选参数是一个用户定义的对象,该对象可向向回调方法传递信息。BeginInvoke立即返回,不等待异步调用完成。BeginInvoke会返回IAsyncResult,这个结果可用于监视异步调用进度。
结果对象IAsyncResult是从开始操作返回的,并且可用于获取有关异步开始操作是否已完成的状态。
结果对象被传递到结束操作,该操作返回调用的最终返回值。
在开始操作中可以提供可选的回调。如果提供回调,在调用结束后,将调用该回调,并且回调中的代码可以调用结束操作。
2.EndInvoke方法检索异步调用的结果
调用BeginInvoke后可随时调用EndInvoke方法。如果异步调用尚未完成,EndInvoke将一直阻止调用线程,直到异步调用完成后才允许调用线程执行。EndInvoke的参数包括需要异步执行的方法的out和ref参数以及由BeginInvoke返回的IAsyncResult。
3.AsyncCallback委托用于指定在开始操作完成后应被调用的方法
AsyncCallback委托被作为开始操作上的第二个到最后一个参数来传递,代码原型如下:
public delegate void AsyncCallback(IAsyncResult ar);
AsyncCallback为客户端应用程序提供完成异步操作的方法。开始异步操作时,该回调委托被提供给客户端。AsyncCallback引用的事件处理程序包含完成客户端异步任务的程序逻辑。
AsyncCallback使用IAsyncResult接口获取异步操作的状态。
4.IAsyncResult接口
它表示异步操作的状态,该接口定义了4个公用属性,代码原型如下:
public interface IAsyncResult
【例1.12】一个异步调用的例子。
新建一个AsyCallEx112的Console应用程序,代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace AsyCallEx112 { class Program { public delegate int sum(int a, int b); //定义一个执行加法的委托 public class number { public int m=4; //定义一个实现此委托签名的方法 public int numberAdd(int a, int b) { int c=a + b; return c; } //定义一个与.net framework定义的AsyncCallback委托相对应的回调方法 public void CallbackMethod2(IAsyncResult ar2) { sum s=(sum)ar2.AsyncState; int number=s.EndInvoke(ar2); m=number; } } static void Main(string[] args) { number num=new number(); sum numberadd=new sum(num.numberAdd); AsyncCallback numberback=new AsyncCallback(num. CallbackMethod2); numberadd.BeginInvoke(55, 33, numberback, numberadd); Console.WriteLine("The sum is:"); Console.WriteLine(num.m); Console.ReadLine(); } } }
输出结果是88。
1.5.3 发起和完成异步调用的方案
实际上,发起和完成异步调用有4种方案可供选择。
1.使用EndInvoke等待异步调用
异步执行方法最简单的方式是通过调用委托的BeginInvoke方法来开始执行方法,在主线程上执行一些工作,然后调用委托的EndInvoke方法。EndInvoke可能会阻止调用线程,因为它直到异步调用完成之后才返回。这种技术非常适合文件或网络操作,但是由于EndInvoke会阻止它,所以不要从服务于用户界面的线程中调用它。
【例1.13】用EndInvoke等待异步调用的例子。
新建一个AsyCallEx113的Console应用程序,代码如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace AsyCallEx113 { class Program { public delegate void AsyncEventHandler(); class Class1 { public void Event1() { Console.WriteLine("Event1 Start"); System.Threading.Thread.Sleep(2000); Console.WriteLine("Event1 End"); } public void Event2() { Console.WriteLine("Event2 Start"); int i=1; while (i < 1000) { i=i + 1; Console.WriteLine("Event2 " + i.ToString()); } Console.WriteLine("Event2 End"); } public void CallbackMethod(IAsyncResult ar) { ((AsyncEventHandler)ar.AsyncState).EndInvoke(ar); } } static void Main(string[] args) { long start=0; long end=0; Class1 c=new Class1(); Console.WriteLine("ready"); start=DateTime.Now.Ticks; AsyncEventHandler asy=new AsyncEventHandler(c.Event1); asy.BeginInvoke(new AsyncCallback(c.CallbackMethod), asy); c.Event2(); end =DateTime.Now.Ticks; Console.WriteLine("时间刻度差="+ Convert.ToString (end-start) ); Console.ReadLine(); } } }
程序异步的处理过程是Event1()和Event2(),程序运行结果如图1-16所示。
图1-16 例1.13程序异步运行结果
把上述程序的异步改成同步,程序运行结果如图1-17所示。
图1-17 例1.13程序同步运行结果
前者的时间刻度大大小于后者,可以明显地看到异步运行的速度优越性。
2.轮询异步调用完成
由BeginInvoke返回的IAsyncResult.IsCompleted属性获取异步操作是否已完成的指示,发现异步调用何时完成,从用户界面的服务线程中进行异步调用时可以执行此操作。调用轮询完成属性(IsCompleted)的线程进行异步调用时,在ThreadPool的线程上执行时可以一直循环执行。
再次修改【例1.13】中主程序的异步调用中的那几行代码:
AsyncEventHandler asy=new AsyncEventHandler(c.Event1); IAsyncResult ia=asy.BeginInvoke(null,null); c.Event2(); while(!ia.IsCompleted) { } asy.EndInvoke(ia);
3.使用WaitHandle等待异步调用
IAsyncResult.AsyncWaitHandle属性获取用于等待异步操作完成的WaitHandle。Wait-Handle.WaitOne方法阻塞当前线程,直到当前的WaitHandle收到信号。
在异步调用完成之后使用WaitHandle,但在调用EndInvoke结果之前,可以执行其他处理。
再次修改【例1.13】主程序的异步调用中的那几行代码:
AsyncEventHandler asy=new AsyncEventHandler(c.Event1); IAsyncResult ia=asy.BeginInvoke(null,null) c.Event2(); ia.AsyncWaitHandle.WaitOne();
4.异步调用完成时执行回调方法
如果启动异步调用的线程是不需要处理结果的线程,则可以在调用完成时执行回调方法。回调方法在ThreadPool线程上执行。
若要使用回调方法,必须将引用回调方法的AsyncCallback委托传递给BeginInvoke。也可以传递包含回调方法将要使用的信息的对象。例如,可以传递启动调用时曾使用的委托,以便回调方法能够调用EndInvoke。
再次修改【例1.13】主程序的异步调用中的那几行代码:
AsyncEventHandler asy=new AsyncEventHandler(c.Event1); asy.BeginInvoke(new AsyncCallback(c.CallbackMethod),asy); c.Event2();
总结:4种使用BeginInvoke和EndInvoke进行异步调用的常用方法,在调用Begin-Invoke之后,可以执行下列操作。
(1)进行某些操作,然后调用EndInvoke一直阻止到调用完成。
(2)使用System.IAsyncResult.AsyncWaitHandle属性获取WaitHandle,使用它的WaitOne方法一直阻止执行直到发出WaitHandle信号,然后调用EndInvoke。
(3)轮询由BeginInvoke返回的IAsyncResult,确定异步调用何时完成,然后调用EndInvoke。
(4)将用于回调方法的委托传递给BeginInvoke。异步调用完成后,将在ThreadPool线程上执行该方法。该回调方法将调用EndInvoke。注意:每次都要调用EndInvoke来完成异步调用。
1.5.4 多线程和方法回调的综合例子
【例1.14】多线程中使用回调的例子。
新建一个ThreadAsyCallEx114的Windows应用程序,界面如图1-18所示,运行结果如图1-19所示。代码如下所示。
图1-18 例1.14程序运行界面
图1-19 例1.14程序运行结果
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Threading; namespace ThreadAsynCallbackEx114 { public partial class Form1 : Form { //声明一个回调函数:注意传递的参数要与Example类中的函数参数类型一致 public delegate void ExampleCallback(int lineCount, Label lb); public Form1() { InitializeComponent(); //CurrentNumber1=new ThreadCurrentNumber(CurrentNumber); } public void CurrentNumber(int tempCurrent, Label lb) { lb.Text=tempCurrent.ToString(); } private void button1_Click(object sender, EventArgs e) { ThreadWithData twd=new ThreadWithData(1, 100, this.label1, new ExampleCallback(CurrentNumber)); Thread td=new Thread(new ThreadStart(twd.RunMethod)); td.Start(); } private void button2_Click(object sender, EventArgs e) { ThreadWithData twd=new ThreadWithData(2, 200, this.label2, new ExampleCallback(CurrentNumber)); Thread td=new Thread(new ThreadStart(twd.RunMethod)); td.Start(); } public class ThreadWithData { private int start=0; private int end=0; private ExampleCallback callBack; private Label lb; public ThreadWithData(int start, int end, Label lb, ExampleCallback callBack) { this.start=start; this.end=end; this.callBack=callBack; this.lb=lb; } public void RunMethod() { for (int i=start; i < end; i++) { Thread.Sleep(500); if (callBack != null) callBack(i, lb); } } } } }
注意在运行时不要按【调试】下的【启动调试(S)】,而是按【开始执行(不调试)】来运行程序。