.Net中的异步编程模式 (APM) (一)

Posted on 2008-09-08 22:23  Nullnoid  阅读(3474)  评论(4编辑  收藏  举报
前段时间看奥运,一下子懒了下来,就停止更新了。本来上一篇,就准备写XAML和Extension的东西,不过最近回顾了前面写的东西,觉得有必要总结一下.Net中的异步编程模式 (APM) 。计划分四个部分:
  • 如何实现支持APM的类
  • 如果实现支持APM的硬件设备类
  • Event-based的APM
  • Continuation-passing Style(CPS)的APM
有些内容相关的文章已经很多了,我写的内容也主要来源于MSDN CONCURRENT AFFAIRS系列。我主要想结合这些内容,看如果使用PowerThreading类库来简化我们的开发 在开始之前,让我们看看异步编程模式APM:

APM简介

APM的概念简单来说就是,主线程创建一个线程执行比较费时的任务,而自己继续执行其他任务。通过使用ThreadPool或者Thread,我们可以很容易的创建线程并让其执行任务,问题的难点在于主线程如何知道该任务是否结束,及如果取消,控制该任务的执行等。

.Net 1.x中定义了IAsyncResult接口,并且类库中会执行费时任务的类都同时提供了同步和异步的API,如FileStream同步读文件的Read方法,对应的异步版本BeginRead和EndRead。主线程调用BeginRead方法时,该方法立即返回一个IAsyncResult对象,而同时FileStream开始执行硬盘读操作。IAsyncResult可以看作的这个读操作的一个"Handler",定义如下:

1 public interface IAsyncResult {
2    WaitHandle AsyncWaitHandle { get; } // 用于等待直到完成方式
3    Boolean    IsCompleted     { get; } // 由于轮询查看方式
4    Object     AsyncState      { get; } // 用于回调方式
5    Boolean    CompletedSynchronously { get; } // 几乎重来不用
6 }

通过这个"Handler"我们可以采用3种方式来等待任务的结束。从这个接口定义,我们可以看出,IAsyncResult方法的局限性。当主线程发出异步任务后,无法取消该任务,也无法知道该任务的执行进度等。我们将后面的内容中,在看如果改善这些问题。

另外,BeginXxx和EndXxx必须成对调用,BeginXxx用于触发任务的执行,EndXxx则用于获得任务执行的返回。尽管有的任务不需要知道结果,但EndXxx还是得调用,否则会造成内存泄漏。如果可以采用回调方式,比较常见的方法就是在回调函数中调用EndXxx。实现回调函数,是比较讨厌的事情,必需通过一定方法,类成员变量,AsyncState等,将FileStream对象传递进去。通过C# 2.0中Anonymous Method及 Limbda,我们可以简化代码,如下面的例子,通过Anonymous Method局部变量的捕获功能,request局部变量漂亮的传递到回调函数中。

 1 // C#2 匿名函数
 2 var request = HttpWebRequest.Create("http://www.google.com");
 3 var result = request.BeginGetResponse(
 4       delegate(IAsyncResult ar_)
 5       {
 6          var response = request.EndGetResponse(ar_);
 7          ProcessData(response);
 8       },
 9       null
10    );
11 
12 // C#3 Limbda
13 result = request.BeginGetResponse( 
14       ar_ => {
15          var response = request.EndGetResponse(ar_);
16          ProcessData(response);
17       },
18       null
19    );

Threadpool与Computing-Bound, I/O-Bound操作

刚开始看CLR via C#中,开始对Computing-Bound operation和IO-Bound operation的区别不是很理解。后来,结合.Net ThreadPool及Window中IO API及thread pool,才弄明白。

首先,我们知道.Net ThreadPool类中的线程分为Worker Thread和I/O Thread。缺省情况下,Worker Thread是CPU*25个,而I/O Thread是1000个。.Net的ThreadPool是基于Windows OS本身提供的Thread Pool的。所以我们先看看Windows本身提供的Thread Pool。

Windows 的Thread Pool (Vista以前)中的线程也分为两类I/O Worker Thread和non-I/O Worker Thread。当我们调用Windows API对I/O如文件进行同步读写时,该线程创建一个IRP的设备请求,并将IRP发送给device stack,然后在核心态等待其完成。而当我们用异步方式时,该线程发送完IRP后,则返回,继续后续的操作。Windows有很多种方式可以通知该I/O操作的完成,与Thread Pool相关的有两种。一种是将完成notification放在该线程的APC队列中,该队列只有当线程进入等待状态是,才会被读取;而另一种方式则是I/O Completion Port,我们可以把这样也认为是个队列,而读取这个队列可以通过GetQueuedCompletionStatus API函数。Windows的Thread Pool的分类就是根据读取不同的队列。I/O Worker Thread读取的是APC队列,也就是当线程完成一个任务后,会进入等待状态;而non-I/O Worker Thread则对应的读取I/O Completion Port队列。APC相比于I/O Completion Port存在很多问题,且性能较差,但是为了向后兼容还是不得不支持。因而在.Net中,只封装了I/O Completion Port,也就是.Net中的I/O Thread对于Windows中的non-I/O Worker Thread。呵呵,希望我还清醒。而APC对于的I/O Worker Thread则没有.Net Thread Pool对应的。

所以在.Net线程池中,I/O Thread实际就是I/O完成端口,而Worker Thread可以看成.Net通过Thread类预先创建的一组线程。.Net及ThreadPool类中提供的方法,如QueueUserWorkItem, Timer, delegate回调等使用的都是Worker Thread。而.Net中对I/O操作的封装,如FileStream, NetworkStream等则是使用的IO Thread。

让我们在回头看CLR via C#中提到的计算约束Computing-Bound和I/O约束I/O-Bound的操作。当我们调用FileStream.BeginRead读文件时,BeginRead并没有创建新的线程去执行读操作,读操作(IRP)被设备执行,同时该线程继续执行其他任务。而当设备完成读操作后,线程池(IO Thread)中的一个线程开始执行回调函数。

而当我们执行的是Computing-Bound的操作时,开始我们就新创建一个线程或通过ThreadPool.QueueUserWorkItem使用线程池中的线程来执行操作,任务完成后,这个线程会执行回调函数。

通过上面的分析,我们可以看到真的不同操作类型,Computing-Bound vs I/O-Bound,类库实现的方式是不同。当然,我们也可以异步Computing-Bound的方式来同步调用FileStream.Read,但是这样我们就没有利用I/O完成端口这个高性能的特性。

本来想这篇就想写如果使用PowerThreading来实现APM的,限于篇幅,就该在下篇吧。希望上面的内容对大家有帮助

参考:

《CLR via C#》第23章 by Jeffery Richard