委托异步编程
原文参考:《.Net 指南》
功能概述
在C#中,委托有两种调用方式:Invoke同步调用;BeginInvoke异步调用。Invoke同步是在当前线程,直接调用委托方法,BeginInvoke则通过线程池中的线程实现异步调用。
常用的异步调用有四种方式:
1. 使用EndInvoke对调用线程进行阻止,直到异步方法调用完成
2. 使用IAsyncResult.AsyncWaitHandle进行调用线程阻止,直到WaitHandle收到信号,然后调用EndInvoke
3. 对BeginInvoke返回的IAsyncResult进行轮询,直到IsComplete属性为True,然后调用EndInvoke
4. 向BeginInvoke传递回调函数,异步方法完成之后,将在异步线程上调用该回调函数,应该在回调函数内调用EndInvoke
!!! 需要注意的是,四种方法都需要调用EndInvoke方法
在介绍四种方法之前,首先做一些准备工作,定义相关测试方法和委托
using System;
using System.Threading;
public class AsyncDemo
{
// 需要进行异步调用的方法
public string TestMethod(int callDuration)
{
//输出当前线程的Id
Console.WriteLine("Test method begins.Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
//模拟一个耗时操作
Thread.Sleep(callDuration);
Console.WriteLine("Test method complete,Thread Id:{0}",Thread.CurrentThread.ManagedThreadId);
return String.Format("My call time was {0}.", callDuration.ToString());
}
}
//定义一个异步委托
public delegate string AsyncMethodCaller(int callDuration);
EndInvoke等待异步调用
在调用BeginInvoke之后,可以随时调用EndInvoke,该方法将阻止调用线程,直到异步方法调用完成,EndInvoke方法同时返回异步方法的返回值,需要注意的是:EndInvoke会阻止调用线程,所以不应该在主线程进行调用!
using System;
using System.Collections.Generic;
using System.Threading;
internal class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Main method begins,Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
AsyncDemo ad = new AsyncDemo();
//创建委托,并将AsyncDemo.TestMethod方法绑定到委托
AsyncMethodCaller asyncCaller = new AsyncMethodCaller(ad.TestMethod);
//开始异步调用
//第一个参数:委托传递的实参
//第二个参数:回调函数
//第三个参数:向IAsyncResult.AsyncState传递的参数
IAsyncResult result = asyncCaller.BeginInvoke(3000, null, null);
//模拟主线程一些工作
Thread.Sleep(1000);
//调用EndInvoke函数,此函数将阻塞当前线程,直到异步方法调用完成,并返回异步方法的返回值
//在实际应用过程中,EndInvoke不应该在主线程进行调用,避免影响UI的响应
string testRtn = asyncCaller.EndInvoke(result);
Console.WriteLine("Async end invoke,return value :{0}",testRtn);
Console.Read();
}
}
输出结果:
Main method begins,Thread ID:1
Test method begins.Thread ID:4
Test method complete,Thread Id:4
Async end invoke,return value :My call time was 3000.
使用WaitHandle等待异步调用
BeginInvoke返回IAsyncResult接口对象中包含WaitHandle类型AsyncWaitHandle成员属性,当异步调用完成之后,WaitHandle信号会被设置,可以通过调用WaitHandle.WaitOne来等待调用完成。
注意:在使用完WaitHandle之后,记得显示调用WaitHandle.Close来释放句柄。
using System;
using System.Collections.Generic;
using System.Threading;
internal class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Main method begins,Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
AsyncDemo ad = new AsyncDemo();
//创建委托,并将AsyncDemo.TestMethod方法绑定到委托
AsyncMethodCaller asyncCaller = new AsyncMethodCaller(ad.TestMethod);
//开始异步调用
//第一个参数:委托传递的实参
//第二个参数:回调函数
//第三个参数:向IAsyncResult.AsyncState传递的参数
IAsyncResult result = asyncCaller.BeginInvoke(3000, null, null);
//模拟主线程一些工作
Thread.Sleep(1000);
Console.WriteLine("Main thread work finish");
//阻塞当前线程,等待异步调用完成
result.AsyncWaitHandle.WaitOne();
//检测异步试试完成了
Console.WriteLine("Do after wait one,IsAyncComplete:{0}", result.IsCompleted);
//在调用EndInvoke获取返回值
string testRtn = asyncCaller.EndInvoke(result);
Console.WriteLine("Async end invoke,return value :{0}", testRtn);
Console.Read();
}
}
输出结果:
Main method begins,Thread ID:1
Test method begins.Thread ID:4
Main thread work finish
Test method complete,Thread Id:4
Do after wait one,IsAyncComplete:True
Async end invoke,return value :My call time was 3000.
轮询IAsyncResult.IsCompleted
在调用线程中,可以通过轮询IAsyncResult.IsCompleted检测异步调用是否完成。
using System;
using System.Collections.Generic;
using System.Threading;
internal class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Main method begins,Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
AsyncDemo ad = new AsyncDemo();
//创建委托,并将AsyncDemo.TestMethod方法绑定到委托
AsyncMethodCaller asyncCaller = new AsyncMethodCaller(ad.TestMethod);
//开始异步调用
//第一个参数:委托传递的实参
//第二个参数:回调函数
//第三个参数:向IAsyncResult.AsyncState传递的参数
IAsyncResult result = asyncCaller.BeginInvoke(3000, null, null);
//模拟主线程一些工作
Thread.Sleep(1000);
//轮询IAsyncResult.IsCompelted
//true表示异步调用已经完成
while (!result.IsCompleted)
{
Thread.Sleep(100);
Console.Write(">");
}
//调用EndInvoke函数
string testRtn = asyncCaller.EndInvoke(result);
Console.WriteLine("Async end invoke,return value :{0}",testRtn);
Console.Read();
}
}
输出结果:
Main method begins,Thread ID:1
Test method begins.Thread ID:4
>>>>>>>>>>>>>>>>>>>Test method complete,Thread Id:4
>Async end invoke,return value :My call time was 3000.
异步回调
在调用BeginInvoke时,可以设置回调方法(签名:void AsyncCallback(IAsyncResult res),该方法在异步方法调用完成时,在异步线程进行调用;同时可以BeginInvoke第三个参数,向回调传递信息。
using System;
using System.Collections.Generic;
using System.Runtime.Remoting.Messaging;
using System.Threading;
internal class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Main method begins,Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
AsyncDemo ad = new AsyncDemo();
//创建委托,并将AsyncDemo.TestMethod方法绑定到委托
AsyncMethodCaller asyncCaller = new AsyncMethodCaller(ad.TestMethod);
//开始异步调用
//第一个参数:委托传递的实参
//第二个参数:回调函数
//第三个参数:向IAsyncResult.AsyncState传递的参数
IAsyncResult result = asyncCaller.BeginInvoke(3000, TestCallback,
"Info from thread:" + Thread.CurrentThread.ManagedThreadId);
//模拟主线程一些工作
Thread.Sleep(1000);
Console.WriteLine("Main thread work complete!");
Console.Read();
}
private static void TestCallback(IAsyncResult res)
{
//通过IAsyncResult.AsyncState获取BeginInvoke传递的参数
string infoFromCaller = (string) res.AsyncState;
Console.WriteLine("Test callback begin,current thread id:{0},state info:{1}", Thread.CurrentThread.ManagedThreadId,
infoFromCaller);
AsyncResult result = res as AsyncResult;
AsyncMethodCaller caller = (AsyncMethodCaller) result.AsyncDelegate;
string testRtn = caller.EndInvoke(res);
Console.WriteLine("Async end invoke,return value :{0}", testRtn);
}
}
输出结果:
Main method begins,Thread ID:1
Test method begins.Thread ID:4
Main thread work complete!
Test method complete,Thread Id:4
Test callback begin,current thread id:4,state info:Info from thread:1 //回调与异步方法同一线程
Async end invoke,return value :My call time was 3000.
关于EndInvoke
根据《.Net指南》要求,在调用BeginInvoke之后,必须调用EndInvoke,其中也没有指出原因,但是根据网上结论,主要是以下三方面:
1. 可能造成内存泄漏(没有任何证据能够证实)
2. 不能捕获异步线程抛出的异常
3. 如果需要获取异步方法的返回参数,必须调用EndInvoke
using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;
public class AsyncDemo
{
// 需要进行异步调用的方法
public string TestMethod(int callDuration)
{
//输出当前线程的Id
Console.WriteLine("Test method begins.Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
//模拟一个耗时操作
Thread.Sleep(callDuration);
throw new Exception("Excep in async thread......");
}
}
//定义一个异步委托
public delegate string AsyncMethodCaller(int callDuration);
internal class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Main method begins,Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
AsyncDemo ad = new AsyncDemo();
//创建委托,并将AsyncDemo.TestMethod方法绑定到委托
AsyncMethodCaller asyncCaller = new AsyncMethodCaller(ad.TestMethod);
//开始异步调用
//第一个参数:委托传递的实参
//第二个参数:回调函数
//第三个参数:向IAsyncResult.AsyncState传递的参数
IAsyncResult result = asyncCaller.BeginInvoke(3000, TestCallback,
"Info from thread:" + Thread.CurrentThread.ManagedThreadId);
//模拟主线程一些工作
Thread.Sleep(1000);
Console.WriteLine("Main thread work complete!");
Console.Read();
}
private static void TestCallback(IAsyncResult res)
{
//通过IAsyncResult.AsyncState获取BeginInvoke传递的参数
string infoFromCaller = (string) res.AsyncState;
Console.WriteLine("Test callback complete,current thread id:{0},state info:{1}",
Thread.CurrentThread.ManagedThreadId,
infoFromCaller);
}
}
输出结果:
Main method begins,Thread ID:1
Test method begins.Thread ID:4
Main thread work complete!
Test callback complete,current thread id:4,state info:Info from thread:1
正确的实现方式是,在回调函数中,调用EndInvoke方法,并将使用try…catch进行包裹,修改回调函数如下:
private static void TestCallback(IAsyncResult res)
{
//通过IAsyncResult.AsyncState获取BeginInvoke传递的参数
string infoFromCaller = (string) res.AsyncState;
Console.WriteLine("Test callback complete,current thread id:{0},state info:{1}",
Thread.CurrentThread.ManagedThreadId,
infoFromCaller);
//在调用EndInvoke时,应该使用try..catch进行包裹
try
{
AsyncResult result = res as AsyncResult;
AsyncMethodCaller caller = (AsyncMethodCaller) result.AsyncDelegate;
caller.EndInvoke(res);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}