【C#進階系列】27 I/O限制的異步操做

上一章講到了用線程池,任務,並行類的函數,PLINQ等各類方式進行基於線程池的計算限制異步操做。數據庫

而本章講的是如何異步執行I/O限制操做,容許將任務交給硬件設備來處理,期間徹底不佔用線程和CPU資源。編程

然而線程池仍然扮演着重要的角色,由於各類I/O操做的結果仍是要由線程池線程來處理。windows

Windows如何執行同步I/O操做數組

既然說道異步I/O操做,那麼首先能夠先看看同步操做是如何執行。安全

就好比操做硬盤上的一個文件,經過構造一個FileStream對象打開磁盤文件,而後調用Read方法從文件讀取數據。服務器

調用Read方法時,線程從託管代碼轉變爲本機/用戶模式代碼,Read內部調用Win32的ReadFile函數。數據結構

ReadFile分配一個小的數據結構,稱爲I/O請求包(I/O Request Packet,IRP)。多線程

IRP結構初始化後包含的內容有:文件句柄,文件中的偏移量(從這個位置開始讀取字節),一個Byte[]數組的地址,要傳輸的字節數以及其它常規性內容。異步

而後ReadFile函數將線程從本機/用戶模式代碼轉變爲本機/內核模式代碼,像內核傳遞IRP,從而調用Windows內核。根據IRP中的設備句柄,Windows內核知道I/O操做要傳送給哪一個硬件設備。async

所以,Windows將IRP傳送給恰當的設備驅動程序的IRP隊列。每一個設備驅動程序都維護本身的IRP隊列,其中包含了機器上運行的全部進程發出的I/O請求。

IRP數據包到達時,設備驅動程序將IRP信息傳遞給物理硬件設備上安裝的電路板。如今,硬件設備將執行請求的I/O操做。

在硬件執行I/O操做期間,發出了I/O請求的線程將無事可作,因此Windows將線程變成睡眠狀態,防止它仍然浪費CPU時間。(然而仍然浪費內存,由於它的用戶模式棧,內核模式棧,線程環境塊和其它數據結構依然在內存中,並且沒有東西訪問這些內存)。

最終硬件設備會完成I/O操做,而後Windows喚醒線程,將其調度給一個CPU,使它從內核模式返回用戶模式,再返回至託管代碼。FileStream的Read方法返回一個Int32,指明從文件中讀取的字節數,使咱們知道在傳給Read的Byte[]中,實際能檢索到多少字節。

對於Web服務器而言,這麼作的話就坑爹了。能夠想象,若是有不少用戶請求服務器,獲取某文件或數據庫的信息,在獲取時線程阻塞,等待返回,那麼就會建立不少線程,若是用戶量足夠大,服務器根本就不夠用。

而當獲取到了信息,大量線程被喚醒,那麼此時就存在大量的線程,而CPU內核通常不會不少,因此就會頻繁切換上下文,這進一步損害了性能。

Windows如何執行異步I/O操做

基於同步I/O操做在某些場景下的坑爹表現, 固然就須要異步操做來解決了。

依然是那個例子,一樣是構造一個FileStream去讀取文件,然而如今傳遞一個FileOptions.Asynchronous標誌,告訴Windows但願用異步方式進行文件讀寫。

而且如今不是調用Read而是ReadAsync來讀取數據。

ReadAsync內部分配一個Task<Int>來表明用於完成讀取操做的代碼。

而後ReadAsync調用Win32 ReadFile函數。

ReadFile分配IRP,和前面同步操做同樣初始化它,而後傳遞給windows內核。

Windows內核將IRP放到驅動程序隊列中,但線程再也不阻塞,而容許返回至你的代碼。(這就是異步的好處了)

因此線程能當即從ReadAsync調用中返回。固然此時IRP還沒有處理好,因此不能在ReadAsync以後的代碼中訪問傳遞的Byte[]中的字節。

ReadAsync以前在內部建立的Task<Int>對象會返回給用戶。

可在該對象上調用ContinueWith來登記任務完成時執行的回調方法,而後在回調函數中處理數據。固然也能夠用C#的異步函數功能簡化代碼,以順序方式寫代碼(感受就像是執行同步I/O)。

硬件設備處理好IRP後,會將IRP放到CLR的線程池隊列,未來某個時候一個線程池線程會提取完成的IRP並/ 成任務的代碼,最終要麼設置異常(若是發生錯誤),要麼返回結果(本例表明成功讀取字節數的一個Int32)

這樣一來,Task對象就知道操做在何時完成,代碼能夠開始運行並安全地訪問Byte[]中的數據。

這樣不阻塞線程使得資源不至於被過分浪費,同時提升了I/O效率。

C#異步函數

在我寫WEB的經歷中歷來沒用過異步函數,卻是之前玩了一段事件Unity3D的時候用過。

實際上在上一章執行定時計算限制操做那個小節就已經用過了,把那個例子粘貼過來了:

      static void Main(string[] args)
        {
            asyncDoSomething();
            Console.Read();
        }

        private static async void asyncDoSomething() {
            while (true) {
                Console.WriteLine("time is {0}", DateTime.Now);
                //不阻塞線程的前提下延遲兩秒
                await Task.Delay(2000);//await容許線程返回
                //2秒後某個線程會在await後介入並繼續循環
            }
        }

這裏的asyncDoSomething這個函數就是異步函數。

它有一個很明顯的標誌,就是用async聲明瞭一下。

異步函數的內部實際上就是使用了Task來實現異步,並且用了一個之前沒有提過的概念:狀態機。

異步函數,顧名思義會異步執行,並且在await後面的操做A通常也是異步執行,且等操做A執行完了,纔會繼續執行await那一行語句後面的語句。

寫法上像一個正常函數,實際上在其內部用Task的ContinueWith去運行恢復狀態機的方法。使Task.Delay(2000)這個線程執行完後,又有一個線程來調用await那行代碼以後的代碼。

使用異步函數要注意如下幾點:

  • 不能將程序的Main函數做爲異步函數。另外構造器,屬性和事件訪問器方法也不能用。
  • 異步函數不能有out和ref參數
  • 不能在catch,finally或unsafe塊中使用await操做符
  • 不能在await操做符以前得到一個支持線程全部權或遞歸的鎖,並在await操做符後釋放它。這是由於await以前的代碼是由一個線程執行,以後的代碼由另外一個線程執行
  • 在查詢表達式中,await操做符只能在初始from子句的第一個集合表達式中使用,或者在join子句的集合表達式使用。

異步函數的返回類型通常是Task或者Task<某類型>,它們表明函數的狀態機完成。(不過也能夠像咱們上面的例子同樣返回void)

事實上,若是異步函數最後return的一個int值,那麼異步函數的返回類型就應該是Task<int>。

通常來說,異步函數都會按規範要求在方法名後附加Async後綴。支持I/O操做的不少類型都提供了Async方法。

在早期版本中,有一個編程模型是使用BeginXxx/EndXxx方法和IAsyncResult接口。

還有一個基於事件的編程模型,提供了XxxAsync方法(不返回Task對象,由於事件都是void)

如今這兩個編程模型都已通過時了,建議用新的以Async結尾的函數的編程模型。(不過仍是有一些類由於微軟沒時間更新,因此這些類只有BeginXxx這種方法)

對於只有BeginXxx和EndXxx的編程模型的類,能夠用Task.Factory.FromAsync方法,將BeginXxx和EndXxx分別做爲參數傳給FromAsync,而後就能夠await Task.Factory.FromAsync(BeginXxx,EndXxx,null)的方式,用新得編程模型了。

應用程序與線程處理模型

.NET支持幾種不一樣的應用程序模型,而每種模型可能引入了它本身的線程處理模型。

控制檯應用程序和Windows服務(實際上也是控制檯應用程序,只是看不到控制檯)沒有引入任何線程處理模型。

而GUI應用程序引入了一個線程處理模型。在此模型中,UI元素只能由建立它的線程更新。

在GUI線程中,常常都須要生成一個異步操做,使GUI線程不至於阻塞並中止響應用戶輸入。但當異步操做完成時,是由一個線程池線程完成Task對象並恢復狀態機。

可是當這個線程池線程一旦更新UI元素就會拋出異常,因此線程池線程只能呢個以某種方式告訴GUI線程更新UI元素。

然而FCL定義了一個SynchronizationContext類(同步上下文類)來解決這個問題,簡單來講此類的對象將應用程序模型和線程處理模型鏈接起來。

做爲開發人員一般不須要了解這個類,等待一個Task時會獲取調用線程的SynchronizationContext對象,線程池完成Task後,會使用該SynchronizationContext對象,確保爲應用程序模型使用正確的線程處理模型。

因此當GUI線程等待一個Task時,await操做符後面的代碼保證在GUI線程上執行,使代碼能正確執行。

Task提供了一個ConfigureAwait方法,向其傳遞true就至關於沒有調用方法,傳遞false則await操做符就不查詢調用線程的SynchronizationContext對象。當線程池結束Task時會直接完成,await操做符後面的代碼經過線程池線程執行。

以異步方式進行I/O操做

以前雖然介紹了異步方式進行I/O操做,實際上那些操做是在內部用另外一個線程模擬異步操做。這個額外的線程也會影響到性能。

若是在建立FileStream對象時,指定FileOptions.Asynchronous標誌,表示以同步仍是異步方式來通訊。

在這個模式下,調用Read,實際上內部也是用異步方式來模擬同步實現。(而實際上若是指定了異步,那麼就用ReadAsync,若是是同步,就用Read,這樣才能獲得最好的性能)

 

PS:

本章實際上的含金量比我寫的這些多很多,能力有限無法徹底寫出來。(信息量較大,我本身都有點迷糊,估計搞完這一輪,還要回過頭來再看看多線程這塊)

特別是在異步函數的狀態機那裏,本書介紹的很詳細,然而我並無寫太多。

主要是做者用了一大片的代碼來解釋,而本人實在懶得抄。

不過相信中心思想仍是提煉出來了,實際上使用了任務,而後功能也至關於把await後面的代碼ContinueWith了。

相關文章
相關標籤/搜索