异步的委托
单纯的委托生成Invoke()方法来调用被代理对象以同步方式维护的方法,也就是说,调用委托的线程会一直等待,直到委托调用完成,再执行后面代码。比如下面这个程序:
namespace SyncDelegateReview
{
public delegate int BinaryOp(int x, int y);
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** Synch Delegate Review *****");
Console.WriteLine("Main() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);
BinaryOp b = new BinaryOp(Add);
int answer = b(10, 10);
Console.WriteLine("Doing more work in Main()!");
Console.WriteLine("10 + 10 is {0}.", answer);
Console.ReadLine();
}
static int add(int x, int y)
{
Console.WriteLine("Add() invoked on thread {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
return x + y;
}
}
}
执行结果:
***** synch delegate review *****
Main() invoked on thread 1.
Add() invoked on thread 1.
Doing more work in main()!
10 + 10 is 20.
press any key to continue . . .
Main()和Add()在一个线程里面调用,并且在Add调用和Doing more work之间会有明显的时间间隔。
BeginInvoke()和EndInvoke()
之前学习委托的时候就说过,定义委托时编译器会自动地创建一个继承自multicastdelegate的类,其中包含begininvoke()和endinvoke()用于异步的调用,比如之前提到的伪码:
public sealed class delegatename : system.multicastdelegate
{
public delegatereturnvalue invoke(alldelegateinputrefandoutparams);
public iasyncresult begininvoke(alldelegateinputrefandoutparams, asynccallback callback, object state);
public delegatereturnvalue endinvoke(alldelegaterefandoutparams, iasyncresult result);
}
和字面意思一样,begininvoke()和endinvoke()是异步委托调用的开关:在调用开始之前,使用begininvoke()方法以使委托在另一个线程中被调用;在需要委托调用的返回结果时,使用endinvoke()方法获得返回值(如果委托没有返回值自然地就不需要在结束时调用endinvoke())。begininvoke()和endinvoke()由一个iasyncresult兼容类型作为耦合。iasyncresult可以视为在调用过程中代表、记录调用情况的一个缓存,后面将详细介绍它。
现在可以使用begininvoke()和endinvoke()方法实现简单的异步委托了:
static void main(string[] args)
{
console.writeline("***** async delegate invocation *****");
console.writeline("main() invoked on thread {0}.", thread.currentthread.managedthreadid);
binaryop b = new binaryop(add);
iasyncresult iftar = b.begininvoke(10, 10, null, null);
console.writeline("doing more work in main()...");
int answer = b.endinvoke(iftar);
console.writeline("10 + 10 is {0}.", answer);
Console.WriteLine();
}
执行结果:
***** Async Delegate Invocation *****
Main() invoked on thread 1.
Doing more work in Main()...
Add() invoked on thread 3.
10 + 10 is 20.
仔细想想会发现,上面这个程序虽然开了两个线程,但是并没有达到“并行”的效果,(在等待子线程运行完成的时间里没有做其它的操作)。
要调用EndInvoke()只能是在调用结束之后,但并发这个东西到底啥时候执行全看系统的心情,写代码的时候是不能预见的,这就给安排并行任务带来了困难。为了能在子线程运行的时间里并行地做一些事情,通常有两个思路:
- 轮询,就是反复地问子线程“你做完了吗做完了吗做完了吗…”
- 回调,调用开始的时候传入一个回调方法,用以在子线程和父线程间通信,当调用完成这个回调函数会被执行。
通过轮询来实现并发的委托
IAsyncResult接口提供IsCompleted属性,返回调用是否完成。在父线程中对这个属性轮询可以一定程度地规避阻塞。看下面对Main的修改:
static void Main(string[] args)
{
...
BinaryOp b = new BinaryOp(Add);
IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null);
while(!iftAR.IsCompleted){
Console.WriteLine("Doing more work in Main()...");
Thread.Sleep(1000);
}
int answer = b.EndInvoke(iftAR);
...
}
也可以用WaitHandle来实现更灵活的等待逻辑:
IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null);
while(!iftAR.AsyncWaitHandle.WaitOne(1000, true))
{
Console.WriteLine("Doing more work in Main()...");
}
int answer = b.EndInvoke(iftAR);
AsyncWaitHandle是IAsyncResult的属性,它返回一个WaitHandle实例,该实例公开了一个名为WaitOne()的方法,它可以指定等待调用执行的时间。在上面这段代码中,如果等待1000时间后调用没有完成,会执行while循坏内的代码。
使用回调方法完成并发的委托
轮询不是一个聪明高效的方法,循环的模式会对它产生限制,在两个线程不断跳转也很浪费资源。使用回调技术是更有效的方法。
当子线程中的任务完成时,线程池会调用回调函数(注意是在次线程调用而不是主线程),以通知父线程“我做完了”。父线程在创建新的线程(子线程)处理业务之后,就继续接下来的任务,(而可以不再管子线程的运行)。
现在来看BeginInvoke()这个方法的进一步功能。
除了和委托签名相同的参数,BeginInvoke()还可以传入两个可选的参数(不用这两个参数时需要传入null,像之前的例子一样),一个AsyncCallback委托实例,一个自定义类(object)用于向回调方法传递信息。AsyncCallback是System命名空间内定义的委托,它接受一个IAsyncResult类型的参数,并且没有返回值;可以发现的是,通过传入的IAsyncResult对象可以在回调方法中获得异步委托的执行情况。最后这个参数可以是自定义的任何类型,它会传入成为返回的IAsyncResult的一个属性AsyncState。
在回调函数中将IAsyncResult强制转换为AsyncResult对象,可以从它的参数AsyncResult.AsyncDelegate获得原始异步委托的引用,这样就能在回调函数中调用EndInvoke()来获得异步委托返回的结果。
需要注意的是,回调函数是在异步委托完成后发起的调用,并且它是由次线程调用的,如果主线程在次线程之前完成了运行而不等待足够的时间,程序可能在次线程完成之前退出。
下面是异步调用完成时执行回调方法的例子:
namespace ASYncCallbackDelegate
{
public delegate int BinaryOp(int x, int y);
class Program
{
private static bool isDone = false;
static void Main(string[] args)
{
Console.WriteLine("***** AsyncCallbackDelegate Example *****");
Console.WriteLine("Main() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);
BinaryOp b = new BinaryOp(Add);
IAdyncResult iftAR = b.BeginInvoke(10, 10, new AsyncCallback(AddComplete), "Main() thanks you for adding these numbers.");
while(!isDone)
{
Thread.Sleep(1000);
Console.WriteLine("Working....");
}
Console.ReadLine();
}
static int Add(int x, int y)
{
Console.WriteLine("Add() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);
return x + y;
}
static void AddCompleted(IAsyncResult itfAR)
{
Console.WriteLine("AddCompleted invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);
AsyncResult ar = (AsyncResult) itfAR;
BianryOp b = (BinaryOp) ar.AsyncDelegate;
Console.WriteLine("10 + 10 is {0}.", b.EndInvoke(itfAR));
isDone = true;
}
}
// 这个程序不是线程安全的,因为有两个线程可能同时读isDone的值,但是就执行而言没什么问题。
}
IAsyncResult接口
到这里已经介绍了IAsyncResult接口的各类属性:
AsyncState | object(自定义类) | 异步调用开始时传入的自定义数据
AsyncWaitHandle | WaitHandle | 用于执行等待逻辑的WaitHandle
CompletedSynchronously | bool | 表示该异步委托是否同步完成
IsCompleted | bool | 表示该异步委托是否已经完成
可以看出,IAsyncResult作为次线程对其它线程暴露的接口,起到线程间交互的作用。
下面引用一段msdn的注释来对它做最后的说明:
The IAsyncResult interface is implemented by classes containing
methods that can operate asynchronously. It is the return type of
methods that initiate an asynchronous operation, such as
FileStream.BeginRead, and it is passed to methods that conclude an
asynchronous operation, such as FileStream.EndRead. IAsyncResult
objects are also passed to methods invoked by AsyncCallback delegates
when an asynchronous operation completes. An object that supports the
IAsyncResult interface stores state information for an asynchronous
operation and provides a synchronization object to allow threads to be
signaled when the operation completes. The AsyncResult class is the
implementation of IAsyncResult that is returned by the BeginInvoke
method when you use a delegate to call a method asynchronously.