程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 【C#進階系列】27 I/O限制的異步操作,

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

編輯:C#入門知識

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


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

而本章講的是如何異步執行I/O限制操作,允許將任務交給硬件設備來處理,期間完全不占用線程和CPU資源。

然而線程池仍然扮演著重要的角色,因為各種I/O操作的結果還是要由線程池線程來處理。

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操作要傳送給哪個硬件設備。

因此,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了。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved