C#网络编程高级篇之网页游戏辅助程序设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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)】,而是按【开始执行(不调试)】来运行程序。