本文内容异步编程类型异步编程模型(APM)参考资料首先澄清,异步编程模式(AsynchronousPRogramming Patterns)与异步编程模型(Asynchronous Programming Model,APM),它们的中文翻译只差一个字,英文名称差在最后一个单词,一个是 Pattern,一个是 Model。模型 Model 比 模式 Pattern 更具体。前者是一个统称,比后者含义要广,前者包含三个模型,而 APM 只是它其中一个而已。
个人理解,异步编程模型(APM)是较底层的一个异步编程模式,在多核时代,这种方式越来越不适用,微软已经不建议使用这种异步方式编程,而是采用基于任务的异步模式(TAP),利用async和await关键字,但如果不了解 APM,就会成为并行编程、并行编程与异步编程相结合以及理解 TAP 的障碍。并行编程是为了多核 CPU。
下载 Demo异步编程模式
.NET Framework 提供了执行异步操作的三种模式:
异步编程模型 (APM) 模式(也称IAsyncResult模式),在此模式中异步操作需要 Begin 和 End 方法(比如用于异步写入操作的 BeginWrite 和 EndWrite)。对于新的开发工作不再建议采用此模式。基于事件的异步模式 (EAP),这种模式需要 Async 后缀,也需要一个或多个事件、事件处理程序委托类型和EventArg派生类型。 EAP 是在 .NET Framework 2.0 中引入的。对于新的开发工作不再建议采用此模式。基于任务的异步模式 (TAP) ,使用一种方法来表示异步操作的启动和完成。TAP 是在 .NET Framework 4 中引入的,并且它是在 .NET Framework 中进行异步编程的推荐使用方法。C# 中的async和await关键词为 TAP 添加了语言支持。本文主要说明异步编程模型(APM)。
异步编程模型(APM)
使用IAsyncResult设计模式的异步操作名为“Begin+操作名称”和“End+操作名称”,这两个方法分别开始和结束异步操作。例如,FileStream类提供BeginRead和EndRead方法从文件异步读取字节。这两个方法实现了Read方法的异步版本。
在调用“Begin+操作名称”方法后,应用程序可以继续在调用线程上执行,同时异步操作在另一个线程上执行。
每次调用“Begin+操作名称”方法时,应用程序还需调用“End+操作名称”方法来获取操作的结果。
环境Windows 7旗舰版 SP1Microsoft Visual Studio Ultimate 2013 Update 4开始异步操作
“Begin+操作名称”方法开始异步操作,并返回实现IAsyncResult接口的对象。IAsyncResult对象存储有关异步操作的信息,其成员如下表所示:
成员
说明
AsyncState
一个可选的应用程序特定的对象,包含有关异步操作的信息。
AsyncWaitHandle
一个 WaitHandle,可用来在异步操作完成之前阻止应用程序执行。
CompletedSynchronously
一个值,指示异步操作是否是在用于调用 Begin操作名称OperationName 的线程上完成,而不是在单独的 ThreadPool 线程上完成。
IsCompleted
一个值,指示异步操作是否已完成。
下面签名是FileStream的异步和同步的 Write 方法:
publicoverrideIAsyncResult BeginWrite(byte[] array,intoffset,intnumBytes, AsyncCallback userCallback,objectstateObject);
publicoverridevoidWrite(byte[] array,intoffset,intcount);
前者是异步方法,后者是同步方法。可以看到,BeginWrite方法具有其同步版本签名中声明的所有参数,即前三个参数。另外两个参数,
第一个是 AsyncCallback委托,此委托引用在异步操作完成时调用的方法。如果调用方不想在操作完成后调用方法,可以指定 null;第二个是一个用户定义的对象。此对象可用来向异步操作完成时调用的方法传递应用程序特定的状态信息。BeginWrite立即返回对调用线程的控制。如果BeginWrite方法引发异常,则会在开始异步操作之前引发异常,这意味着没有调用回调方法。
结束异步操作以FileStream的EndWrite方法为例:
publicoverridevoidEndWrite(IAsyncResult asyncResult);
EndWrite方法结束异步写操作。EndWrite方法的返回值与其同步版本的返回值类型相同,并且是特定于异步操作的。除了来自同步方法的参数外,EndWrite方法还包括IAsyncResult参数。调用方必须将对应调用返回的实例传递给EndWrite。如果调用EndWrite时,IAsyncResult对象表示的异步操作尚未完成,则EndWrite将在异步操作完成之前阻止调用线程。异步操作引发的异常是从EndWrite方法引发的。
说明:此设计模式的实施者应通知调用方异步操作已通过以下步骤完成:将IsCompleted设置为 true,调用异步回调方法(如果已指定一个回调方法),然后发送 AsyncWaitHandle 信号。
对于访问异步操作的结果,开发人员有若干种选择。正确的选择取决于应用程序是否有可以在操作完成时执行的指令。如果应用程序在接收到异步操作结果之前不能进行任何其他工作,则必须先阻止该应用程序进行其他工作,等到获得这些操作结果后再继续进行。若要在异步操作完成之前阻止应用程序,您可以使用下列方法之一:
在应用程序的主线程调用 End***,阻止应用程序执行,直到操作完成之后再继续。参看 Demo 中的“BlockUntilOperationCompletes”项目;用AsyncWaitHandle来阻止应用程序执行,直到一个或多个操作完成之后再继续执行。参考 Demo 中的“WaitUntilOperationCompletes”项目。
在异步操作完成时不需要阻止的应用程序可使用下列方法之一:
轮询操作完成状态:定期检查IsCompleted属性,操作完成后调用 End***。参看 Demo 中的“PollUntilOperationCompletes”项目;使用 AsyncCallback 委托来指定操作完成时要调用的方法。参看 Demo 中的“UseDelegateForAsyncCallback”项目。以上是利用 .Net Framework 类库 APM 模型提供的异步方式,你需要做的是,利用类库中 Begin 和 End 开头方法,指定AsyncCallback回调方法和你自己定义的一个对象(一般为封装的状态信息),并用IAsyncResult来获取异步的状态来完成异步编程。
因为用户定义的委托具有Invoke、BeginInvoke和EndInvoke方法,所以下面说明如何利用自定义的委托和IAsyncResult,将你自己的同步方法变成异步方法,加深对异步的理解。
使用委托进行异步编程用户定义的委托具有Invoke、BeginInvoke和EndInvoke方法。使用委托可以通过异步方式调用同步方法。
当同步调用一个委托时,Invoke方法直接对当前线程调用目标方法。
如果调用BeginInvoke方法,则公共语言运行时 (CLR) 会对请求进行排队并立即返回到调用方。 会对来自线程池的线程异步调用目标方法。 提交请求的原始线程自由地继续与目标方法并行执行。
如果在对BeginInvoke方法的调用中指定了回调方法,则当目标方法结束时将调用该回调方法。 在回调方法中,EndInvoke方法获取返回值和所有输入/输出参数或仅供输出参数;否则,未指定任何回调方法,则可以从调用BeginInvoke的线程中调用 EndInvoke。
注意:编译器应使用由用户指定的委托签名发出具有Invoke、BeginInvoke和EndInvoke方法的委托类。 应将BeginInvoke和EndInvoke方法修饰为本机方法。 因为这些方法被标记为本机的,所以 CLR 在类加载时自动提供该实现。加载程序确保它们未被重写。
使用异步方式调用同步方法.NET Framework 允许您异步调用任何方法。定义委托的签名与委托的目标方法的签名要相同;公共语言运行时会自动使用适当的签名为该委托定义BeginInvoke和EndInvoke方法。
BeginInvoke方法启动异步调用。该方法与您需要异步执行的方法具有相同的参数,另外两个可选参数:第一个是AsyncCallback委托,该委托引用在异步调用完成时要调用的方法;第二个是用户定义的对象,该对象将信息传递到回调方法。BeginInvoke立即返回,不等待异步调用完成。BeginInvoke返回一个IAsyncResult,后者可用于监视异步调用的进度。
EndInvoke方法检索异步调用的结果。在调用BeginInvoke之后随时可以调用该方法。如果异步调用尚未完成,则EndInvoke会一直阻止调用线程,直到异步调用完成。EndInvoke参数包括需要异步执行方法中的 out 和 ref 参数以及由BeginInvoke返回的IAsyncResult。
下面代码示例演示了使用BeginInvoke和EndInvoke进行异步调用的四种常用方法。调用BeginInvoke之后,您可以执行下列操作:
进行某些操作,然后调用EndInvoke一直阻止到调用完成;使用IAsyncResult.AsyncWaitHandle属性获取WaitHandle,使用其WaitOne方法阻止执行,直至WaitHandle收到信号,然后调用EndInvoke;轮询由BeginInvoke返回的IAsyncResult,以确定异步调用何时完成,然后调用EndInvoke;将用于回调方法的委托传递给BeginInvoke。 异步调用完成后,将在 ThreadPool 线程上执行该方法。 回调方法调用EndInvoke。注意:无论您使用何种方法,都要调用 EndInvoke 来完成异步调用。
定义测试方法和异步委托下面代码示例演示异步调用一个长时间运行的方法TestMethod的各种方式。TestMethod方法会显示一条控制台消息,说明该方法已开始处理,休眠几秒钟后结束。TestMethod有一个 out 参数,用于说明此类参数添加到BeginInvoke和EndInvoke的签名中的方式。
publicclassAsyncDemo
{
// The delegate must have the same signature as the method
// it will call asynchronously.
publicdelegatestringAsyncMethodCaller(intcallDuration,outintthreadId);
// The method to be executed asynchronously.
publicstringTestMethod(intcallDuration,outintthreadId)
{
Console.WriteLine("Test method begins.");
Thread.Sleep(callDuration);
threadId = Thread.CurrentThread.ManagedThreadId;
returnString.Format("My call time was {0}.", callDuration.ToString());
}
}
使用 EndInvoke 等待异步调用异步执行一个方法,最简单方式是通过调用委托的BeginInvoke方法来开始执行方法,在主线程上执行一些操作,然后调用委托的EndInvoke方法。EndInvoke可能会阻止调用线程,因为该方法直到异步调用完成后才返回。这种方式很适合执行文件或网络操作。
注意:因为 EndInvoke 可能会阻塞,所以不应从服务于用户界面的线程调用该方法。
usingSystem;
usingSystem.Threading;
usingAsynchronousOperations;
namespaceUseEndInvokeToWaitAsyncCall
{
classProgram
{
staticvoidMain(string[] args)
{
// The asynchronous method puts the thread id here.
intthreadId;
// Create an instance of the test class.
AsyncDemo ad =newAsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller =newAsyncDemo.AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000,outthreadId,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.
stringreturnValue = caller.EndInvoke(outthreadId, 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.
使用 WaitHandle 等待异步调用使用由BeginInvoke返回的IAsyncResult的AsyncWaitHandle属性来获取WaitHandle。
异步调用完成时,WaitHandle会收到信号。
如果使用WaitHandle,则在异步调用完成之前或之后,只要在调用EndInvoke之前,还可以执行其他处理。
说明:调用EndInvoke时不会自动关闭等待句柄。若要在WaitHandle等待句柄使用完后,立即释放系统资源,调用WaitHandle.Close方法。显式释放可释放的对象时,垃圾回收的工作效率会更高。
usingSystem;
usingSystem.Threading;
usingAsynchronousOperations;
namespaceUseWaiHandleToWaitAsyncCall
{
classProgram
{
staticvoidMain(string[] args)
{
// The asynchronous method puts the thread id here.
intthreadId;
// Create an instance of the test class.
AsyncDemo ad =newAsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller =newAsyncDemo.AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000,outthreadId,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.
stringreturnValue = caller.EndInvoke(outthreadId, 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属性来判断异步调用是否完成。从用户界面的服务线程中进行异步调用时可以执行此操作。轮询完成(只要IsCompleted为 false)允许调用线程继续执行,而异步调用在一个线程池线程执行。
usingSystem;
usingSystem.Threading;
usingAsynchronousOperations;
namespacePollUntilAsyncComplete
{
classProgram
{
staticvoidMain(string[] args)
{
// The asynchronous method puts the thread id here.
intthreadId;
// Create an instance of the test class.
AsyncDemo ad =newAsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller =newAsyncDemo.AsyncMethodCaller(ad.TestMethod);
// Initiate the asychronous call.
IAsyncResult result = caller.BeginInvoke(3000,outthreadId,null,null);
// Poll while simulating work.
while(result.IsCompleted ==false)
{
Thread.Sleep(250);
Console.Write(".");
}
// Call EndInvoke to retrieve the results.
stringreturnValue = caller.EndInvoke(outthreadId, 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。
usingSystem;
usingSystem.Runtime.Remoting.Messaging;
usingSystem.Threading;
usingAsynchronousOperations;
namespaceWhenAsyncIsCompletedRunAsyncCallback
{
classProgram
{
staticvoidMain(string[] args)
{
// Create an instance of the test class.
AsyncDemo ad =newAsyncDemo();
// Create the delegate.
AsyncDemo.AsyncMethodCaller caller =newAsyncDemo.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.
intdummy = 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,
outdummy,
newAsyncCallback(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 theapplicationrunning
// 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.
staticvoidCallbackMethod(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.
stringformatString = (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.
intthreadId = 0;
// Call EndInvoke to retrieve the results.
stringreturnValue = caller.EndInvoke(outthreadId, 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 参数,因此 TestMethod 从不使用该参数的输入值。 会将一个虚拟变量传递给 BeginInvoke 调用。 如果 threadId 参数为 ref 参数,则该变量必须为类级别字段,这样才能同时传递给 BeginInvoke 和 EndInvoke。传递给 BeginInvoke 的状态信息是一个格式字符串,回调方法使用该字符串来设置输出消息的格式。 因为作为类型 Object 进行传递,所以状态信息必须强制转换为正确的类型才能被使用。回调在 ThreadPool 线程上执行。 ThreadPool 线程是后台线程,这些线程不会在主线程结束后保持应用程序的运行,因此示例的主线程必须休眠足够长的时间以便回调完成。参考资料Microsoft Developer Network 异步编程模式Microsoft Developer Network 异步编程模型
下载 Demo