你的位置:首页 > ASP.net教程

[ASP.net教程].NET 底层异步编程模式异步编程模型(Asynchronous Programming Model,APM)


本文内容

  • 异步编程类型
  • 环境
  • 异步编程模型(APM)
  • 参考资料

首先澄清,异步编程模式(Asynchronous Programming Patterns)与异步编程模型(Asynchronous Programming Model,APM),它们的中文翻译只差一个字,英文名称差在最后一个单词,看英文一个是 Pattern,一个是 Model。Model 比 Pattern 更具体。前者是一个统称,比后者含义要广,前者包含三个模型,而 APM 只是它其中一个而已。

个人理解,异步编程模型(APM)是较底层的一个异步编程模式,在多核时代,这种方式越来越不适用,微软已经不建议使用这种异步方式,但如果不了解 APM,就会成为并行编程、并行编程与异步编程相结合的障碍。并行编程是为了多核 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 添加了语言支持。

本文主要说明异步编程模型。

环境


  • Windows 7 旗舰版 SP1
  • Microsoft Visual Studio Ultimate 2013 Update 4

异步编程模型(APM)


使用 IAsyncResult 设计模式的异步操作名为“Begin+操作名称”和“End+操作名称”,这两个方法分别开始和结束异步操作。例如,FileStream 类提供 BeginRead 和 EndRead 方法从文件异步读取字节。这两个方法实现了 Read 方法的异步版本。

在调用“Begin+操作名称”方法后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。每次调用“Begin+操作名称”方法时,应用程序还应调用“End+操作名称”方法来获取操作的结果。

开始异步操作

“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 方法可结束异步写操作。EndWrite 方法的返回值与其同步版本的返回值类型相同,并且是特定于异步操作的。除了来自同步方法的参数外,EndWrite 方法还包括 IAsyncResult 参数。 调用方必须将对应调用返回的实例传递给 Begin操作名称OperationName。

如果调用 EndWrite 时,IAsyncResult 对象表示的异步操作尚未完成,则 EndWrite 将在异步操作完成之前阻止调用线程。异步操作引发的异常是从 EndWrite 方法引发的。

说明:此设计模式的实施者应通知调用方异步操作已通过以下步骤完成:将 IsCompleted 设置为 true,调用异步回调方法(如果已指定一个回调方法),然后发送 AsyncWaitHandle 信号。

对于访问异步操作的结果,应用程序开发人员有若干种设计选择。正确的选择取决于应用程序是否有可以在操作完成时执行的指令。如果应用程序在接收到异步操作结果之前不能进行任何其他工作,则必须先阻止该应用程序进行其他工作,等到获得这些操作结果后再继续进行。若要在异步操作完成之前阻止应用程序,您可以使用下列方法之一:

  • 从应用程序的主线程调用 End***,阻止应用程序执行,直到操作完成之后再继续执行。参看 Demo 中的“BlockUntilOperationCompletes”项目;
  • 使用 AsyncWaitHandle 来阻止应用程序执行,直到一个或多个操作完成之后再继续执行。参考 Demo 中的“BlockAppByAsyncWaitHandle”项目。

在异步操作完成时不需要阻止的应用程序可使用下列方法之一:

  • 轮询操作完成状态:定期检查 IsCompleted 属性,操作完成后调用 End***。参看 Demo 中的“PollUntilOperationCompletes”项目;
  • 使用 AsyncCallback 委托来指定操作完成时要调用的方法。参看 Demo 中的“UseDelegateForAsyncCallback”项目。

示例

下面示例演示向文件写入数据后,读取并验证。只有写完成,才能读;全部读完,才能验证。

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) 以获取为这些方法定义的参数的说明。

下面的代码示例演示了使用 BeginInvokeEndInvoke 进行异步调用的四种常用方法。调用 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 会收到信号,您可以通过调用 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 线程是后台线程,这些线程不会在主线程结束后保持应用程序的运行,因此示例的主线程必须休眠足够长的时间以便回调完成。

参考资料


  • Microsoft Developer Network 异步编程模式

 

 下载 Demo