文章目錄
- APM簡介
- Threadpool與Computing-Bound, I/O-Bound操作
前段時間看奧運,一下子懶了下來,就停止更新了。本來上一篇,就準備寫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 RichardImplementing the CLR Asynchronous Programming ModelSimplified APM with C#Windows I/O threads vs. managed I/O threads