简介
执行异步操作是构建高性能、可扩展应用程序的关键,它允许我们能够用非常少的线各执行许多操作。MS CLR团队设计了一种模式,可以让开发人员方便地利用这种能力,这种模式称为异步编程模式(APM)。
在FCL中许多类型都支持APM,如下面一些具体例子:
- 所有派生自System.IO.Stream并与硬件设计通信的类都提供了BeginRead和BeginWrite方法。
- System.Net.Dns类提供了BeginGetHostAddresses、BeginGetHostByName、BeginGetHostEntry和BeginResolve方法。
- 所有派生自System.Net.WebRequest的类都提供BeginGetRequestStream和BeginGetResponse方法。
另外,所有的委托类型都定义了一个BeginInvoke方法来使用APM。
使用APM执行受I/O限制的异步操作
如果希望使用APM从一个文件流中异步地读取一些字节。首先,需要调用System.IO.FileStream对象的构造器并接受一个System.IO.FileOptions参数来构建一个System.IO.FileStream对象。对于System.IO.FileOptions参数来说,我们传递一个FileOptions.Asynchronous标记,该标记告诉FileStream对象,我们准备在文件上执行异步读/写操作。
为从FileStream对象中同步地读取字节,我们可以调用它的Read方法,该方法的原型如下:
public Int32 Read(Byte[] array, Int32 offset, Int32 count);
同步方法的问题在于,直到所读取的字节都已存放到字节数组中,方法才会返回,效率很低。
为从文件中异步地读取字节,可以调用FileStream的BeginRead方法:
IAsyncResult BeginRead(Byte[] array, Int32 offset, Int32 numBytes, AsyncCallback userCallback, Object stateObject);
注意,BeginRead方法的前三个参数与Read方法参数相同。BeginRead方法实际上将操作请求加入到Window设备驱动程序的队列中,而Windows的设备驱动程序知道如何与正确的硬件设备通信。就这样硬件接管了该操作,也不需要任何线程来执行任何操作,甚至不需要等待输出结果,这种方法相当高效。
其他类型的I/O操作与文件I/O操作的工作相似。如使用NetworkStream时,I/O请求加入到Windows的网络设备驱动程序的队列,然后在此等待响应。
BeginRead方法返回一个其类型实现了System.IAsyncResult接口的对象的引用。调用BeginRead方法时,它构建一个对象来唯一地标识I/O操作请求,并将请求加入Windows设备驱动程序的队列,然后将IAsyncReault对象返回给我们。我们可以将IAsyncResult对象看作收据。当BeginRead方法返回时,I/O操作只是被排队,它还没有完成。因此,我们还不能操作字节数组中的字节,因为数组中还没有包含所请求的数据。
实际上,数组中可能已经包含了所请求的数据,因为I/O操作已经被异步地执行了。但数据也有可能在几秒种后才从服务器读取过来。还有可能服务器网络已瘫痪,数据永远也不会读取过来。我们需要一种方法来发现实际上发生了哪一种情况,而且还要知道结果是什么时候检测到的。我们将这种情况称为异步操作的聚集。APM提供了三种异步操作结果的聚集技巧。
创建FileStream对象时,可以通过FileOptions.Asynchronous标记来指定希望使用同步操作通信还是异步操作通信(与调用Win32的CreateFile函数并为它传递一个FILE_FLAG_OVERLAPPED标记功能相同)。如果我们没有指定这个标记,那么Windows将在文件上同步执行所有的操作。当然,我们仍可以调用FileStream的BeginRead方法,对于应用程序来说,看起来好像操作是异步执行的,但是,实际上,FileStream类内部调用另一个线程来模仿异步行为。这个额外的线程是不经济的,而且还影响性能。
另一方面,可以通过指定FileOptions.Asynchronous标记来创建一个FileStream对象,然后可以调用FileStream的Read方法来执行同步操作。而File-Stream类内部通过启动一个异步操作,然后立即将调用线程睡眠直到操作完成来模仿同步行为。这种方式效率极低,但相对于不指定FileOptions.Asynchronous标记来创建FileStream对象,然后调用BeginRead方法,效率又要好些。
总而言之,使用FileStream对象时,应首先要决定准备在文件上执行同步I/O操作还是异步I/O操作,并通过指定FileOptions.Asynchronous标记(或不指定)来表明我们的选择。如果指定了这个标记,通常应调用BeginRead方法。如果没有指定这个标记,通常应调用Read方法。这样将使程序获得最佳的性能。
APM的三个聚集技巧
APM支持以下三种聚集技巧:等待直至完成(wait-until-done)、轮询(polling)和方法回调(method callback)。
APM的等待直至完成聚集技巧
为启动一个异步操作,我们可以调用一些BeginXxx方法。所有这些方法都会将请求操作排队,然后返回一个IAsyncResult对象来标识挂起的操作。为获得操作的结果,我们可以以IAsyncResult对象为参数调用相应的EndXxx方法。所有的EndXxx方法都接受一个IAsyncResult对象作为它的一个参数。基本而言,在调用EndXxx方法时,我们是在请求CLR返回由IAsyncResult对象标识的异步操作的结果。
如果异步操作已完成,那么在调用EndXxx方法时,它将立即返回结果。如果异步操作还没完成,EndXxx方法将挂起调用线程直至异步操作完成,然后返回结果。
FileStream的EndXxx方法原型如下:
Int32 EndRead(IAsyncResult asyncResult);
方法的返回值表示从FileStream对象中所读取的字节的数量。
完整的FileStream范例如下:
using System;
using System.IO;
using System.Threading;
public static class Program
{
public static void Main()
{
//打开指示异步I/O操作的文件
FileStream fs = new FileStram(@"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);
//在这里执行一些代码
//挂起该线程直至异步操作结束并获得结果
Int32 bytesRead = fs.EndRead(ar);
//已经没有操作执行任务,关闭文件
fs.Close();
//访问字节数组并显示结果
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));
}
}
如果在BeginRead和EndRead间放入一些代码,会看到APM的一些价值。下面的代码对前面的程序进行了实质性的修改。新版本的程序同时从多个流中读取数据。
private sealed class AsyncStreamRead
{
private Stream m_stream;
private IAsyncResult m_ar;
private Byte[] m_data;
public AsyncStreamRead(Stream stream, Int32 numBytes)
{
m_stream = stream;
m_data = new Byte[numBytes];
//为Stream对象初始化一个异步读操作
m_ar = stream.BeginRead(m_data, 0, numBytes, null, null);
}
public Byte[] EndRead()
{
//挂起该线程直至异步操作结束并获得结果
Int32 numBytesRead = m_stream.EndRead(m_ar);
//已经没有操作执行任务,关闭流
m_stream.Close();
//调整数组的大小以节省空间
Array.Resize(ref m_data, numBytesRead);
//返回字节
return m_data;
}
}
private static void ReadMultipleFiles(params String[] pathnames)
{
AsyncStreamRead[] asrs = new AsyncStreamRead[pathnames.Length];
for(Int32 n = 0; n < pathnames.Length; n++)
{
//打开指示异步I/O操作的文件
Stream stream = new FileStream(pathnames[n], FileMode.Open, FileAccess.Read, FileShare.Read, 1024, FileOptions.Asynchronous);
//为Stream对象初始化一个异步读操作
asrs[n] = new AsyncStreamRead(stream, 100);
}
//所有的流都已打开,而且所有的读请求都已排队,它们都同时并发执行
//下面获取并显示结果
for(Int32 n = 0; n < asrs.Length; n++)
{
Byte[] bytesRead = asrs[n].EndRead();
//现在可以访问字节数组并显示结果
Console.WriteLine("Number of bytes read={0}", bytesRead.Length);
Console.WriteLine(BitConverter.ToString(bytesRead));
}
}
APM的轮询聚集技巧
IAsyncResult接口定义了若干只读属性:
public interface IAsyncResult
{
Object AsyncState {get;}
WaitHandle AsyncWaitHandle {get;}
Boolean IsCompleted {get;}
Boolean CompletedSynchronously {get;}
}
使用轮询聚集技巧的效率不高。编写使用轮询聚集技巧的代码时,要让一个线程定期地询问CLR查看异步请求是否已经完成执行。因此,实质上是浪费了一个线程的时间。示例如下:
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);
}
//获取结果。注意:EndRead方法不能挂起这个线程
Int32 bytesRead = fs.EndRead(ar);
//访问数组并显示结果
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));
}
下面是另一个轮询聚集技巧的范例。该范例查询了IAsyncResult接口的AsyncWaitHandle属性:
public static void PollingWithAsyncWaitHandle()
{
//打开指示异步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.AsyncWaitHandle.WaitOne(10, false))
{
Console.WriteLine("Operation not completed; still waiting.");
}
//获取结果,注意:EndRead方法不能挂起这个线程
Int32 bytesRead = fs.EndRead(ar);
//关闭文件
fs.Close();
//访问数组并显示结果
Console.WriteLine("Number of bytes read={0}", bytesRead);
Console.WriteLine(BitConverter.ToString(data, 0, bytesRead));
}
IAsyncResult接口的AsyncWaitHandle属性返回一个派生自WaitHandle的对象的引用,该对象通常为System.Threading.ManualResetEvent。
APM的方法回调聚集技巧
所有的APM聚集技巧中,方法回调聚集技巧最好用。原因在于该技巧永远不会将一个线程置于等待状态,而且该技巧不会定期地检查异步操作是否已完成而浪费CPU时间。
下面是方法回调聚集技巧的基本工作原理:首先将异步I/O请求排队等候,然后线程继续执行它希望执行的任何事情。接着,当I/O请求完成时,Windows将工作项加入CLR的线程池的队列中。最后,线程池中的线程将工作项从队列中取出,并调用我们编写的一些方法。现在,在回调方法内部,我们首先调用EndXxx方法来获得异步操作的结果,然后就可以自由地继续处理结果。当回调方法返回时,线程池中的线程返回到线程池中准备服务下一个排队的工作项(或等待下一个工作项的出现)。
每个BeginXxx方法的最后两个参数是相同的,一个是System.AsyncCallback,它是一个委托类型,定义如下所示:
delegate void AsyncCallback(IAsyncResult ar);
该委托表示必须实现的回调方法所需的签名。对于BeginXxx方法的stateObject参数,可传递我们希望传递的任何参数。该参数只是提供一种方式,即将操作排队的方法中的一些数据传递到处理操作完成的回调方法中。回调方法将接收一个IAsyncResult对象的引用,而且回调方法可以通过查询IAsyncResult的AsyncState属性来获得状态对象的引用。示例代码如下:
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);
//为FileStream对象初始化一个异步读操作,并将FileStream对象fs传递给回调方法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);
//从IAsyncResult对象中提取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(data, 0, bytesRead));
}
}