执行异步操作是构建高性能、可扩展性应用程序的关键,它允许我们用非常少的线程执行许多操作。加上线程池,异步操作允许我们利用机器中的所有CPU。微软意识到这里面会存在许多潜在问题,所以设计了一种模式来方便的利用这些能力,即异步编程模式(APM)。
APM是一个单一的模式,但允许用于异步执行受限计算和受I/O限制的操作。在FCL中有许多类型都支持它。例如所有的委托类型都定义了一个BeginInvoke方法来使用APM。还有很多类也都提供了BeginXXX方法和EndXXX方法。
使用APM执行受I/O限制的异步操作。
如果我们想从一个文件流中读取一些字节。需要用到类System.IO.FileStream。如果同步的读取字节,可以调用他的Read方法。调用这个方法时,读操作同步发生,直到所读取的字节都放到数组中,方法才会返回。但是I/O操作的时间很难预测,并且在等待I/O操作完成时,调用线程被挂起,因此效率很低而且有可能因为某些物理原因导致异常抛出。
采用异步读取字节,可以调用FileStream的BeginRead方法。因为要执行I/O操作,所以BeginRead方法实际上将操作请求加入到Windows设备驱动程序的队列中,而Windows的设备驱动程序知道如何域正确的硬件设备通信。这样,硬件接管了该操作,也不需要任何线程来执行任何操作,甚至还不需要等待输出结果,此方法相当高效。
BeginRead方法返回一个实现了IAsyncResult接口的对象的引用。调用BeginRead方法时,它构建一个对喜爱那个来唯一的标识I/O操作请求,并将请求加入到Windows设备驱动程序队列,然后将IAsyncResult对象返回。当BeginRead方法返回时,I/O操作只是被排队,还没有完成。因此我们需要一种方法来查看I/O操作发生了哪种情况,还要知道结果时什么时候检测到的。这种情况称为异步操作结果的聚集(Rendezvousing)。
APM的主要特征就是提供了三个聚集技巧。如在使用Threadpool的QueueUserWorkItem方法时,CLR并没有内置的方法来供我们查找异步操作何时完成的。另外CLR也没有内置方法来发现异步操作的结果。APM提供了三种机制,使我们可以判断异步操作是什么时候完成的,而且允许我们获得异步操作的结果。
1、等待直至完成(wait-until-done)
在调用BeginXXX方法后,为了获取操作的结果,可以以IAsyncResult对象为参数调用EndXXX方法。如果异步操作已经完成,他可以立即返回结果,如果操作没有完成,该方法将挂起调用线程直至异步操作完成,然后返回结果。如果采用这种方法,可以在Begin和End方法之间加入我们想要执行的其他代码,来体现APM的价值。
2、轮询(polling)
IAsyncResult接口提供了若干只读属性:
public interface IAsyncResult {
Object AsyncState { get; }
WaitHandle AsyncWaitHandle { get; }
Boolean IsCompleted { get; }
Boolean CompletedSynchronously { get; }
}
可以通过查询这几个属性来判断异步操作的状态。轮询聚集的技巧执行效率并不高,因此不建议使用。但是如果愿意用一些效率来换取编程的简单方便,那么论询技巧在某些时候时很好用的。示例代码:
public static void PollingWithIsCompleted() {
//打开指示异步I/O操作的文件
FileStream fs = new FileStream(@"C:\Boot.ini", FileMode.Open,
FileAccess.Read, FileShare.Read, 1024,
FileOptions.Asynchronous);
Byte[] data = new Byte[100];
// 为FileStream初始化一个异步操作
IAsyncResult ar = fs.BeginRead(data, 0, data.Length, null, null);
//判断
while (!ar.IsCompleted) {
Console.WriteLine("Operation not completed; still waiting.");
Thread.Sleep(10);
}
// 获取结果
Int32 bytesRead = fs.EndRead(ar);
fs.Close();
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));
}
3、使用方法回调
在构建高性能和可扩展的应用程序时,方法回调最好用。他永远不会将一个线程置于等待状态(与等待区别),而且该技巧还不回定期的检查异步操作是否已经完成而浪费CPU时间(与轮询区别)。
工作原理:首先将异步I/O请求排队等候,然后线程继续执行它希望执行的事情。当I/O请求完成时,Windows将工作项加入CLR的线程池的队列中。最后,线程池中的线程将工作项从队列中取出,并调用我们编写的一些方法(通过这种方式我们就知道异步I/O操作已经完成)。在回调方法内部,我们首先调用EndXXX方法获得异步操作的结果,然后就可以自由的继续进行处理。当回调方法返回时,线程池中的线程返回到线程池中准备服务下一个排队的工作项。
示例代码:
using System;
using System.IO;
using System.Threading;
public static class Program {
// 静态数组
private static Byte[] s_data = new Byte[100];
public static void Main() {
// 执行Main方法的线程ID
Console.WriteLine("Main thread ID={0}",
Thread.CurrentThread.ManagedThreadId);
// 打开只是异步I/O操作的文件
FileStream fs = new FileStream(@"C:\Boot.ini", FileMode.Open,
FileAccess.Read, FileShare.Read, 1024,
FileOptions.Asynchronous);
// 初始化异步操作
// Pass the FileStream (fs) to the callback method (ReadIsDone)
fs.BeginRead(s_data, 0, s_data.Length, ReadIsDone, fs);
// 执行其他代码
Console.ReadLine() ;
}
private static void ReadIsDone(IAsyncResult ar) {
// 显示执行ReadIsDone方法的线程ID
Console.WriteLine("ReadIsDone thread ID={0}",
Thread.CurrentThread.ManagedThreadId);
// 获取filestream对象
FileStream fs = (FileStream) ar.AsyncState;
// 获取结果
Int32 bytesRead = fs.EndRead(ar);
fs.Close();
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(s_data, 0, bytesRead));
}
}
使用APM执行受计算限制的异步操作
APM执行异步I/O的主要特点就是没有线程运行或者等待I/O操作的完成。除此之外,APM也可以用于执行受计算限制的异步操作。
定义一个希望调用的方法int Sum(int i);
定义一个与该方法拥有相同个签名的委托 delegate int SumDelegate(int i);在定义委托后,编译器就会为委托生成一个包含BeginInvoke和EndInvoke的类。
初始化委托变量调用委托的BeginInvoke方法。
如:public static void Main() {
// 初始化委托变量
SumDelegate SumDelegate = Sum;
//调用使用线程池中线程的方法
sumDelegate.BeginInvoke(100, SumIsDone, sumDelegate);
Console.ReadLine();
}
可以看出,与通常情况下线程池中线程从执行方法的过程中返回后返回线程池不同,当Sum方法返回时,他将调用BeginInvoke的第二个参数的方法SumIsDone。
private static void SumIsDone(IAsyncResult ar) {
SumDelegate sumDelegate = (SumDelegate) ar.AsyncState;
int sum = sumDelegate.EndInvoke(ar);
Console.WriteLine("Sum={0}", sum);
}
APM几点说明
必须调用EndXXX方法,否则可能泄露资源。
对于任何给定的异步操作,调用EndXXX方法的次数不应超过一次。
调用BeginXXX方法时,无论使用什么对象,都必须使用同一个对象来调用EndXXX方法。
Windows窗口线程
在Windows中,一个窗口通常由一个线程创建,而且还必须使用这个线程来处理这个窗口的所有动作。因为Windows窗体构建在Windows系统之上,所以不允许线程池中的线程直接操作窗口,或者不能直接操作派生自Controls的类。
在Winfrom中,Controls类提供了三个方法Invoke、BeginInvoke、EndInvoke。我们可以从任何线程中调用这三个方法将调用线程中的操作封送处理到创建窗口的线程中。
在WPF中,可以通过System.Windows.Threading.Dispatch类来实现分发处理。