- 如何实现支持APM的类
- 如果实现支持APM的硬件设备类
- Event-based的APM
- Continuation-passing Style(CPS)的APM
APM简介
APM的概念简单来说就是,主线程创建一个线程执行比较费时的任务,而自己继续执行其他任务。通过使用ThreadPool或者Thread,我们可以很容易的创建线程并让其执行任务,问题的难点在于主线程如何知道该任务是否结束,及如果取消,控制该任务的执行等。
.Net 1.x中定义了IAsyncResult接口,并且类库中会执行费时任务的类都同时提供了同步和异步的API,如FileStream同步读文件的Read方法,对应的异步版本BeginRead和EndRead。主线程调用BeginRead方法时,该方法立即返回一个IAsyncResult对象,而同时FileStream开始执行硬盘读操作。IAsyncResult可以看作的这个读操作的一个"Handler",定义如下:
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局部变量漂亮的传递到回调函数中。
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操作
首先,我们知道.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的,限于篇幅,就该在下篇吧。希望上面的内容对大家有帮助
参考: