首先澄清,异步编程模式(Asynchronous Programming Patterns)与异步编程模型(Asynchronous Programming Model,APM),它们的中文翻译只差一个字,英文名称差在最后一个单词,看英文一个是 Pattern,一个是 Model。Model 比 Pattern 更具体。前者是一个统称,比后者含义要广,前者包含三个模型,而 APM 只是它其中一个而已。
个人理解,异步编程模型(APM)是较底层的一个异步编程模式,在多核时代,这种方式越来越不适用,微软已经不建议使用这种异步方式,但如果不了解 APM,就会成为并行编程、并行编程与异步编程相结合的障碍。并行编程是为了多核 CPU。
.NET Framework 提供了执行异步操作的三种模式:
本文主要说明异步编程模型。
使用 IAsyncResult 设计模式的异步操作名为“Begin+操作名称”和“End+操作名称”,这两个方法分别开始和结束异步操作。例如,FileStream 类提供 BeginRead 和 EndRead 方法从文件异步读取字节。这两个方法实现了 Read 方法的异步版本。
在调用“Begin+操作名称”方法后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。每次调用“Begin+操作名称”方法时,应用程序还应调用“End+操作名称”方法来获取操作的结果。
“Begin+操作名称”方法开始异步操作,并返回实现 IAsyncResult 接口的对象。 IAsyncResult 对象存储有关异步操作的信息,其成员如下表所示:
成员
说明
AsyncState
一个可选的应用程序特定的对象,包含有关异步操作的信息。
AsyncWaitHandle
一个 WaitHandle,可用来在异步操作完成之前阻止应用程序执行。
CompletedSynchronously
一个值,指示异步操作是否是在用于调用 Begin操作名称OperationName 的线程上完成,而不是在单独的 ThreadPool 线程上完成。
IsCompleted
一个值,指示异步操作是否已完成。
下面签名是 FileStream 的异步和同步的 Write 方法:
monospace; width: 100%; border-bottom-style: none; color: black; padding-bottom: 0px; direction: ltr; text-align: left; padding-top: 0px; border-right-style: none; padding-left: 0px; margin: 0em; border-left-style: none; line-height: 12pt; padding-right: 0px; background-color: white">public override IAsyncResult BeginWrite(byte[] array, int offset, int numBytes, AsyncCallback userCallback, object stateObject);
public override void Write(byte[] array, int offset, int count);
前者是异步方法,后者是同步方法。可以看到,BeginWrite 方法具有该方法同步版本签名中声明的所有任何参数,即前三个参数。另外两个参数,
BeginWrite 立即返回对调用线程的控制。如果 BeginWrite 方法引发异常,则会在开始异步操作之前引发异常,这意味着没有调用回调方法。
以 FileStream 的 EndWrite 方法为例:
public override void EndWrite(IAsyncResult asyncResult);EndWrite 方法可结束异步写操作。EndWrite 方法的返回值与其同步版本的返回值类型相同,并且是特定于异步操作的。除了来自同步方法的参数外,EndWrite 方法还包括 IAsyncResult 参数。 调用方必须将对应调用返回的实例传递给 Begin操作名称OperationName。
如果调用 EndWrite 时,IAsyncResult 对象表示的异步操作尚未完成,则 EndWrite 将在异步操作完成之前阻止调用线程。异步操作引发的异常是从 EndWrite 方法引发的。
说明:此设计模式的实施者应通知调用方异步操作已通过以下步骤完成:将 IsCompleted 设置为 true,调用异步回调方法(如果已指定一个回调方法),然后发送 AsyncWaitHandle 信号。
对于访问异步操作的结果,应用程序开发人员有若干种设计选择。正确的选择取决于应用程序是否有可以在操作完成时执行的指令。如果应用程序在接收到异步操作结果之前不能进行任何其他工作,则必须先阻止该应用程序进行其他工作,等到获得这些操作结果后再继续进行。若要在异步操作完成之前阻止应用程序,您可以使用下列方法之一:
在异步操作完成时不需要阻止的应用程序可使用下列方法之一:
下面示例演示向文件写入数据后,读取并验证。只有写完成,才能读;全部读完,才能验证。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace APMDemo
{
/// <summary>
/// 异步向文件写数据,之后验证数据正确性
/// 写完才能读取,读取完才能验证
/// </summary>
class Program
{
static void Main(string[] args)
{
// 创建一个异步对象,当验证完成后,获得信号
// ManualResetEvent 通知一个或多个正在等待的线程已发生事件。
ManualResetEvent manualEvent = new ManualResetEvent(false);
// 创建随机数据写入文件
byte[] writeArray = new byte[100000];
new Random().NextBytes(writeArray);
FileStream fStream = new FileStream("Test#@@#.dat", FileMode.Create, FileAccess.ReadWrite, FileShare.None, 4096, true);
// 检查 FileStream 被异步打开
Console.WriteLine("fStream was {0}opened asynchronously.", fStream.IsAsync ? "" : "not ");
// 异步写入文件
IAsyncResult asyncResult = fStream.BeginWrite(writeArray, 0, writeArray.Length,
new AsyncCallback(EndWriteCallback),
new State(fStream, writeArray, manualEvent));
// 并发做些其他事情……等待写入和验证数据
manualEvent.WaitOne(5000, false);
Console.ReadKey();
}
/// <summary>
/// 向数据写入文件,当 BeginWrite 完成时,
/// EndWriteCallback 方法会被调用,结束异步写操作,并且读取和验证数据
/// </summary>
/// <param name="asyncResult"></param>
static void EndWriteCallback(IAsyncResult asyncResult)
{
State tempState = (State)asyncResult.AsyncState;
FileStream fStream = tempState.FStream;
fStream.EndWrite(asyncResult);
// 异步读取已写入的数据
fStream.Position = 0;
asyncResult = fStream.BeginRead(tempState.ReadArray, 0, tempState.ReadArray.Length,
new AsyncCallback(EndReadCallback), tempState);
// 并发做点其他事情……如日志操作
}
/// <summary>
/// 从文件读取数据,当 BeginRead 完成时,
/// EndReadCallback 方法被调用,结束异步读取操作,然后验证数据
/// </summary>
/// <param name="asyncResult"></param>
static void EndReadCallback(IAsyncResult asyncResult)
{
State tempState = (State)asyncResult.AsyncState;
int readCount = tempState.FStream.EndRead(asyncResult);
int i = 0;
while (i < readCount)
{
if (tempState.ReadArray[i] != tempState.WriteArray[i++])
{
Console.WriteLine("Error writing data.");
tempState.FStream.Close();
return;
}
}
Console.WriteLine("The data was written to {0} and verified.", tempState.FStream.Name);
tempState.FStream.Close();
// 当验证完成后,通知主线程
tempState.ManualEvent.Set();
}
/// <summary>
/// 维护状态信息,传递给 EndWriteCallback 和 EndReadCallback
/// </summary>
class State
{
// fStream 用于读写文件
FileStream fStream;
// writeArray 存储向写入的数据
byte[] writeArray;
// readArray 存储从文件读取数据
byte[] readArray;
// 当验证完成后,manualEvent 通知主线程
ManualResetEvent manualEvent;
public State(FileStream fStream, byte[] writeArray, ManualResetEvent manualEvent)
{
this.fStream = fStream;
this.writeArray = writeArray;
this.manualEvent = manualEvent;
readArray = new byte[writeArray.Length];
}
public FileStream FStream
{ get { return fStream; } }
public byte[] WriteArray
{ get { return writeArray; } }
public byte[] ReadArray
{ get { return readArray; } }
public ManualResetEvent ManualEvent
{ get { return manualEvent; } }
}
}
}
这是借助 .net framework 提供的异步方法,然后,你利用 IAsyncResult 来完成你自己的异步操作。
下面说明如何利用委托和 IAsyncResult,将你自己的同步方法变成异步方法。通过下面示例,你就会对异步的实现有所了解。
使用委托可以通过异步方式调用同步方法。 当同步调用一个委托时,Invoke 方法直接对当前线程调用目标方法。 如果调用 BeginInvoke 方法,则公共语言运行时 (CLR) 会对请求进行排队并立即返回到调用方。 会对来自线程池的线程异步调用目标方法。 提交请求的原始线程自由地继续与目标方法并行执行。 如果在对 BeginInvoke 方法的调用中指定了回调方法,则当目标方法结束时将调用该回调方法。 在回调方法中,EndInvoke 方法获取返回值和所有输入/输出参数或仅供输出参数。 如果在调用 BeginInvoke 时未指定任何回调方法,则可以从调用 BeginInvoke 的线程中调用 EndInvoke。
注意:编译器应使用由用户指定的委托签名发出具有 Invoke、BeginInvoke 和 EndInvoke 方法的委托类。 应将 BeginInvoke 和 EndInvoke 方法修饰为本机方法。 因为这些方法被标记为本机的,所以 CLR 在类加载时自动提供该实现。 加载程序确保它们未被重写。
.NET Framework 允许您异步调用任何方法。 为此,应定义与您要调用的方法具有相同签名的委托;公共语言运行时会自动使用适当的签名为该委托定义 BeginInvoke 和 EndInvoke 方法。
BeginInvoke 方法启动异步调用。 该方法与您需要异步执行的方法具有相同的参数,还有另外两个可选参数。 第一个参数是一个 AsyncCallback 委托,该委托引用在异步调用完成时要调用的方法。 第二个参数是一个用户定义的对象,该对象将信息传递到回调方法。 BeginInvoke 立即返回,不等待异步调用完成。 BeginInvoke 返回一个 IAsyncResult,后者可用于监视异步调用的进度。
EndInvoke 方法检索异步调用的结果。 在调用 BeginInvoke 之后随时可以调用该方法。 如果异步调用尚未完成,则 EndInvoke 会一直阻止调用线程,直到异步调用完成。 EndInvoke 参数包括需要异步执行方法中的out 和 ref 参数(在 Visual Basic 中为 <Out>< > ByRef和ByRef)以及由BeginInvoke 返回的IAsyncResult 。
说明:Visual Studio 2005 中的 IntelliSense 功能显示 BeginInvoke 和 EndInvoke 的参数。 如果您没有使用 Visual Studio 或类似工具,或您使用的是带有 Visual Studio 2005 的 C#,请参见 异步编程模型 (APM) 以获取为这些方法定义的参数的说明。
下面的代码示例演示了使用 BeginInvoke 和 EndInvoke 进行异步调用的四种常用方法。调用 BeginInvoke 之后,您可以执行下列操作:
注意:无论您使用何种方法,都要调用 EndInvoke 来完成异步调用。
下面的代码示例演示异步调用同一个长时间运行的方法 TestMethod 的各种方式。 TestMethod 方法会显示一条控制台消息,说明该方法已开始处理,休眠了几秒钟,然后结束。 TestMethod 有一个 out 参数,用于说明此类参数添加到 BeginInvoke 和 EndInvoke 的签名中的方式。
public class AsyncDemo
{
// The delegate must have the same signature as the method
// it will call asynchronously.
public delegate string AsyncMethodCaller(int callDuration, out int threadId);
// The method to be executed asynchronously.
public string TestMethod(int callDuration, out int threadId)
{
Console.WriteLine("Test method begins.");
Thread.Sleep(callDuration);
threadId = Thread.CurrentThread.ManagedThreadId;
return String.Format("My call time was {0}.", callDuration.ToString());
}
}
异步执行方法的最简单方式是通过调用委托的 BeginInvoke 方法来开始执行方法,在主线程上执行一些操作,然后调用委托的 EndInvoke 方法。 EndInvoke 可能会阻止调用线程,因为该方法直到异步调用完成后才返回。 这种方式非常适合执行文件或网络操作。
注意:因为 EndInvoke 可能会阻塞,所以不应从服务于用户界面的线程调用该方法。
using System;
using System.Threading;
using AsynchronousOperations;
namespace UseEndInvokeToWaitAsyncCall
{
class Program
{
static void Main(string[] args)
{
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller = new AsyncDemo.AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null, null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.", Thread.CurrentThread.ManagedThreadId);
// Call EndInvoke to wait for the asynchronous call to complete,
// and to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
Console.WriteLine("The call executed on thread {0}, with return value \"{1}\".", threadId, returnValue);
Console.WriteLine("Press any Key to Exit.");
Console.ReadKey();
}
}
}
//This example produces output similar to the following:
//Main thread 10 does some work.
//Test method begins.
//The call executed on thread 6, with return value "My call time was 3000.".
//Press any Key to Exit.
您可以使用由 BeginInvoke 返回的 IAsyncResult 的 AsyncWaitHandle 属性来获取 WaitHandle。 异步调用完成时,WaitHandle 会收到信号,您可以通过调用 WaitOne 方法等待它。
如果您使用 WaitHandle,则在异步调用完成之前或之后,但在通过调用 EndInvoke 检索结果之前,还可以执行其他处理。
说明:调用 EndInvoke 时不会自动关闭等待句柄。 如果释放对等待句柄的所有引用,则当垃圾回收功能回收等待句柄时,将释放系统资源。 若要在等待句柄使用完毕后立即释放系统资源,请调用 WaitHandle.Close 方法来释放等待句柄。 显式释放可释放的对象时,垃圾回收的工作效率会更高。
using System;
using System.Threading;
using AsynchronousOperations;
namespace UseWaiHandleToWaitAsyncCall
{
class Program
{
static void Main(string[] args)
{
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller = new AsyncDemo.AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null, null);
Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.", Thread.CurrentThread.ManagedThreadId);
// Wait for the WaitHandle to become signaled.
result.AsyncWaitHandle.WaitOne();
// Perform additional processing here.
// Call EndInvoke to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
// Close the wait handle.
result.AsyncWaitHandle.Close();
Console.WriteLine("The call executed on thread {0}, with return value \"{1}\".", threadId, returnValue);
Console.WriteLine("Press any Key to Exit.");
Console.ReadKey();
}
}
}
//This example produces output similar to the following:
//Main thread 8 does some work.
//Test method begins.
//The call executed on thread 9, with return value "My call time was 3000.".
//Press any Key to Exit.
您可以使用由 BeginInvoke 返回的 IAsyncResult 的 IsCompleted 属性来发现异步调用何时完成。 从用户界面的服务线程中进行异步调用时可以执行此操作。 轮询完成允许调用线程在异步调用在 ThreadPool 线程上执行时继续执行。
using System;
using System.Threading;
using AsynchronousOperations;
namespace PollUntilAsyncComplete
{
class Program
{
static void Main(string[] args)
{
// The asynchronous method puts the thread id here.
int threadId;
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller = new AsyncDemo.AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000, out threadId, null, null);
// Poll while simulating work.
while (result.IsCompleted == false)
{
Thread.Sleep(250);
Console.Write(".");
}
// Call EndInvoke to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, result);
Console.WriteLine("\nThe call executed on thread {0}, with return value \"{1}\".", threadId, returnValue);
Console.WriteLine("Press any Key to Exit.");
Console.ReadKey();
}
}
}
//This example produces output similar to the following:
//Test method begins.
//............
//The call executed on thread 10, with return value "My call time was 3000.".
//Press any Key to Exit.
如果启动异步调用的线程不需要是处理结果的线程,则可以在调用完成时执行回调方法。 回调方法在 ThreadPool 线程上执行。
若要使用回调方法,必须将表示回调方法的 AsyncCallback 委托传递给 BeginInvoke。 也可以传递包含回调方法要使用的信息的对象。 在回调方法中,可以将 IAsyncResult(回调方法的唯一参数)强制转换为 AsyncResult 对象。 然后,可以使用 AsyncResult.AsyncDelegate 属性获取已用于启动调用的委托,以便可以调用 EndInvoke。
using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using AsynchronousOperations;
namespace WhenAsyncIsCompletedRunAsyncCallback
{
class Program
{
static void Main(string[] args)
{
// Create an instance of the test class.
AsyncDemo ad = new AsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller = new AsyncDemo.AsyncMethodCaller(ad.TestMethod);
// The threadId parameter of TestMethod is an out parameter, so
// its input value is never used by TestMethod. Therefore, a dummy
// variable can be passed to the BeginInvoke call. If the threadId
// parameter were a ref parameter, it would have to be a class-
// level field so that it could be passed to both BeginInvoke and
// EndInvoke.
int dummy = 0;
// Initiate the asynchronous call, passing three seconds (3000 ms)
// for the callDuration parameter of TestMethod; a dummy variable
// for the out parameter (threadId); the callback delegate; and
// state information that can be retrieved by the callback method.
// In this case, the state information is a string that can be used
// to format a console message.
IAsyncResult result = caller.BeginInvoke(3000,
out dummy,
new AsyncCallback(CallbackMethod),
"The call executed on thread {0}, with return value \"{1}\".");
Console.WriteLine("The main thread {0} continues to execute...", Thread.CurrentThread.ManagedThreadId);
// The callback is made on a ThreadPool thread. ThreadPool threads
// are background threads, which do not keep the application running
// if the main thread ends. Comment out the next line to demonstrate
// this.
Thread.Sleep(4000);
Console.WriteLine("The main thread ends.");
Console.WriteLine("Press any Key to Exit.");
Console.ReadKey();
}
// The callback method must have the same signature as the
// AsyncCallback delegate.
static void CallbackMethod(IAsyncResult ar)
{
// Retrieve the delegate.
AsyncResult result = (AsyncResult)ar;
AsyncDemo.AsyncMethodCaller caller = (AsyncDemo.AsyncMethodCaller)result.AsyncDelegate;
// Retrieve the format string that was passed as state
// information.
string formatString = (string)ar.AsyncState;
// Define a variable to receive the value of the out parameter.
// If the parameter were ref rather than out then it would have to
// be a class-level field so it could also be passed to BeginInvoke.
int threadId = 0;
// Call EndInvoke to retrieve the results.
string returnValue = caller.EndInvoke(out threadId, ar);
// Use the format string to format the output message.
Console.WriteLine(formatString, threadId, returnValue);
}
}
}
//This example produces output similar to the following:
//Test method begins.
//The main thread 9 continues to execute...
//The call executed on thread 6, with return value "My call time was 3000.".
//The main thread ends.
//Press any Key to Exit.
说明:
- TestMethod 的 threadId 参数为 out 参数(在 Visual Basic 中为 <Out> ByRef ),因此 TestMethod从不使用该参数的输入值。 会将一个虚拟变量传递给 BeginInvoke 调用。 如果 threadId 参数为 ref 参数(在 Visual Basic 中为 ByRef),则该变量必须为类级别字段,这样才能同时传递给 BeginInvoke 和 EndInvoke。
- 传递给 BeginInvoke 的状态信息是一个格式字符串,回调方法使用该字符串来设置输出消息的格式。 因为作为类型 Object 进行传递,所以状态信息必须强制转换为正确的类型才能被使用。
- 回调在 ThreadPool 线程上执行。 ThreadPool 线程是后台线程,这些线程不会在主线程结束后保持应用程序的运行,因此示例的主线程必须休眠足够长的时间以便回调完成。
下载 Demo