.NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)
本文内容
- 异步编程类型
- 异步编程模型(APM)
- 参考资料
首先澄清,异步编程模式(Asynchronous Programming 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 旗舰版 SP1
- Microsoft Visual Studio Ultimate 2013 Update 4
开始异步操作
“Begin+操作名称”方法开始异步操作,并返回实现 IAsyncResult 接口的对象。 IAsyncResult 对象存储有关异步操作的信息,其成员如下表所示:
成员 | 说明 |
AsyncState | 一个可选的应用程序特定的对象,包含有关异步操作的信息。 |
AsyncWaitHandle | 一个 WaitHandle,可用来在异步操作完成之前阻止应用程序执行。 |
CompletedSynchronously | 一个值,指示异步操作是否是在用于调用 Begin操作名称OperationName 的线程上完成,而不是在单独的 ThreadPool 线程上完成。 |
IsCompleted | 一个值,指示异步操作是否已完成。 |
下面签名是 FileStream 的异步和同步的 Write 方法:
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 方法具有其同步版本签名中声明的所有参数,即前三个参数。另外两个参数,
- 第一个是 AsyncCallback 委托,此委托引用在异步操作完成时调用的方法。如果调用方不想在操作完成后调用方法,可以指定 null;
- 第二个是一个用户定义的对象。此对象可用来向异步操作完成时调用的方法传递应用程序特定的状态信息。
BeginWrite 立即返回对调用线程的控制。如果 BeginWrite 方法引发异常,则会在开始异步操作之前引发异常,这意味着没有调用回调方法。
结束异步操作
以 FileStream 的 EndWrite 方法为例:
public override void EndWrite(IAsyncResult asyncResult);
如果调用 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 的签名中的方式。
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());
}
}
使用 EndInvoke 等待异步调用
异步执行一个方法,最简单方式是通过调用委托的 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.
使用 WaitHandle 等待异步调用
使用由 BeginInvoke 返回的 IAsyncResult 的 AsyncWaitHandle 属性来获取 WaitHandle。
异步调用完成时,WaitHandle 会收到信号。
如果使用 WaitHandle,则在异步调用完成之前或之后,只要在调用 EndInvoke 之前,还可以执行其他处理。
说明:调用 EndInvoke 时不会自动关闭等待句柄。若要在 WaitHandle 等待句柄使用完后,立即释放系统资源,调用 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 属性来判断异步调用是否完成。从用户界面的服务线程中进行异步调用时可以执行此操作。轮询完成(只要 IsCompleted 为 false)允许调用线程继续执行,而异步调用在一个线程池线程执行。
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 参数,因此 TestMethod 从不使用该参数的输入值。 会将一个虚拟变量传递给 BeginInvoke 调用。 如果 threadId 参数为 ref 参数,则该变量必须为类级别字段,这样才能同时传递给 BeginInvoke 和 EndInvoke。
- 传递给 BeginInvoke 的状态信息是一个格式字符串,回调方法使用该字符串来设置输出消息的格式。 因为作为类型 Object 进行传递,所以状态信息必须强制转换为正确的类型才能被使用。
- 回调在 ThreadPool 线程上执行。 ThreadPool 线程是后台线程,这些线程不会在主线程结束后保持应用程序的运行,因此示例的主线程必须休眠足够长的时间以便回调完成。
参考资料