.NET有三種異步模式程式設計能力。
基于任務的異步模式(TAP )Task-based Asynchronous Pattern
該模式使用單一方法表示異步操作的開始和完成,async 和 await 關鍵詞為TAP添加了支援
TAP 在<code>.NET Framework 4</code>中引入
在 .NET 中異步程式設計推薦的方法
基于事件的異步模式(EAP)Event-based Asynchronous Pattern
該模型是舊模型,異步行為是基于事件的
這種模式需要字尾為 Async 的方法、一個或多個事件、事件處理的委托類型、EventArg派生類型。
EAP 在<code>.NET Framework 2.0</code> 中引入
不建議再使用
異步程式設計模型(APM)Asynchronous Programming Model
該模型是舊模型,使用IAsyncResult提供異步行為,也稱為 IAsyncResult 模式
該模式下,同步操作需要 Begin 和 End 方法
APM 在<code>.NET Framework 1.0</code>中引入
TAP建議用于新開發。命名空間在<code>System.Threading.Tasks</code>中。TAP的異步方法和同步方法具有相同的簽名。但是有 <code>out</code>和 <code>ref</code>參數除外,并且應該避免它,将其作為<code>Task<T></code>的一部分傳回。
TAP設計中也可以增加取消的支援,如果操作允許取消,需要增加<code>CancellationToken</code> 類型參數。
TAP設計中也可以增加進度通知的支援,需要 <code>IProgress<T></code>類型參數。
在TAP中,<code>async</code> 和<code>await</code> 關鍵字可以異步調用和阻塞異步方法的調用。
下面用一個示例示範TAP基本用法。例子中有 <code>Person</code> 類,類有同步和異步方法。
<code>Listen Music</code> 同步方法和 <code>PlayGame</code>同步方法。
示範1:同步調用。
運作結果:同步調用,在主線程順序輸出。
示範2:異步調用。
<code>ListenMusicAsync</code> 和<code>PlayGameAsync</code> 的方法定義:
運作結果:
就像調用普通方法一樣,方法就可以異步執行。有時候異步調用存在先後順序。此時在調用異步操作的方法聲明加上 <code>async</code> 關鍵字,調用異步方法使用 <code>await</code> 關鍵字,等待異步的操作傳回,然後再繼續執行。
示範3:阻塞調用。
調用異步的外部方法添加 <code>async</code> 關鍵字,調用異步的時候使用 <code>await</code> 關鍵字,同時傳回值不是<code>Task<string></code> 而是 <code>string</code> 。
然後調用 <code>AsyncRunBlock</code> 方法,運作結果如下:
看起來和同步調用一樣,但是還是有差別的。同步方法調用聽歌和玩遊戲都在主線程中順序執行。異步方法使用TAP,在執行聽歌和玩遊戲,其實都開啟了另外的線程來執行(示範2)并不在主線程,然後我們在主線程控制了兩個異步線程的前後順序。這種技術在用戶端和Web網頁開發中極其有用,下載下傳等耗時操作不應卡死界面,應該放在UI線程之外來做。
示範4:可取消和進度通知。
進度通知的事件:
取消異步程式執行還有其他的用法,比如:
運作結果:在聽歌到30%的時候任務被取消,但是玩遊戲的任務沒有取消仍繼續運作,
一般用于執行多個任務,同時仍能響應使用者互動的場景。實際上,在 <code>System.Threading</code> 中提供了高性能多線程的所有工具,但是有效使用它需要豐富的經驗,而且所需要的工作相對較多。如果是簡單的多線程應用程式,<code>BackgroundWorker</code> 比較适合,因為它是一種簡單的多線程解決方案。對于複雜的異步應用,可以考慮使用基于事件的異步模式EAP。
EAP的目标在于讓開發者像使用事件一樣來編寫異步的程式,并且可以支援并行執行多個操作。每個操作完成後會收到通知。EAP設計規範上還支援異步取消操作(當取消時,如果正好異步操作執行結束,就會發生“競争條件”)。
在基于事件的異步模式(EAP)中,可設計為單調用和多調用兩種方式。通過重載方法添加一個額外<code>object</code>類型參數來實作。額外參數的核心目的是辨別多調用情況下的執行個體,便于後續的的跟蹤。對應的,取消異步的方法,在多調用的情況下,也要有額外的<code>object</code>參數。
在基于事件的異步模式(EAP)中可以增加進度和增量的跟蹤事件。多調用情況下需要識别調用的執行個體。
下面用一個示例示範EAP的基本用法。例子中有有一個 <code>Boy</code> 類,類中一個同步的 <code>ListenMusic</code> 方法和一個異步的<code>ListenMusicAsync</code> 方法。作為對比,還有一個同步的 <code>PlayGame</code> 方法。
<code>ListenMusic</code> 和 <code>PlayGame</code> 同步方法定義
異步<code>ListenMusicAsync</code> 方法和取消方法,外加一個測試異常的方法。方法中的代碼并不十分符合面向對象規範,在此隻示範用法。
以下是完成事件的參數定義:
示範1:同步調用
運作結果:順序在主線程執行。
示範2:異步調用
通知回調的代碼如下:
運作結果如下:聽歌和玩遊戲同時進行,定期會收到進度的通知,聽歌結束後會收到事件通知。
示範3:異步取消
通知回調的代碼和上面一樣,在開始聽歌後取消。
運作結果:使用者取消聽歌後,歌曲播放就結束了。
示範4:異常
在異步線程發生異常後,通知事件中的屬性不可通路。試圖通路會引發異常。
值得注意的是,如果使用者取消異步操作,會正常觸發 <code>ListenMusicCompleted</code> 結束事件,回調參數中 <code>Cancelled</code> 值是<code>True</code>。此時回調參數中的屬性依然不能通路,通路的話會引發上述異常。其實不難了解,使用者都取消任務了,再通路屬性将變的毫無意義。如果在實作EAP過程中<code>AsyncCompletedEventArgs</code>屬性不添加 <code>RaiseExceptionIfNecessary</code> 方法檢驗,那麼通路屬性異常不會發生。這是不建議的。對異步程式來說,有可能會隐藏好多難以發現的問題,建議按照官方推薦方式來實作EAP。
官方描述。
一般原則,盡量使用EAP,如果無法滿足一些要求,可能還需要實作 APM (IAsyncResult模式)。 何時實作 EAP 推薦指南: 将基于事件的模式用作公開類的異步行為的預設 API。 如果類主要用于用戶端應用(例如,Windows 窗體),請勿公開IAsyncResult模式。 僅在需要滿足特定要求時,才公開IAsyncResult 模式。 例如,為了與現有 API 相容,可能需要公開IAsyncResult 模式。 請勿在不公開基于事件的模式的情況下公開 IAsyncResult 模式。 如果必須公開IAsyncResult 模式,請以進階選項的形式這樣做。 例如,如果生成代理對象,預設生成的是基于事件的模式,并含用于生成IAsyncResult 模式的選項。 在IAsyncResult 模式實作的基礎之上生成基于事件的模式實作。 避免對相同的類公開基于事件的模式和IAsyncResult 模式。 請對“進階”類公開基于事件的模式,并對“低級”類公開IAsyncResult 模式。 例如,比較 WebClient 元件上基于事件的模式與 HttpRequest 類上的IAsyncResult 模式。 出于相容性需要,可以對相同的類公開基于事件的模式和IAsyncResult 模式。 例如,如果已釋放使用IAsyncResult 模式的 API,需要保留IAsyncResult 模式,以實作向後相容性。 如果生成的對象模型複雜性遠遠超過分離實作的好處,請對相同的類公開基于事件的模式和IAsyncResult 模式。 對一個類公開兩種模式優于避免公開基于事件的模式。 如果必須對一個類公開基于事件的模式和IAsyncResult 模式,請将EditorBrowsableAttribute設定為 Advanced,以将IAsyncResult 模式實作标記為進階功能。 這會訓示設計環境(如 Visual Studio IntelliSense)不顯示IAsyncResult 屬性和方法。 這些屬性和方法仍完全可用,這樣做隻是為了讓使用 IntelliSense 的開發人員對 API 更加明确。 何時公開 IAsyncResult 模式的條件: IAsyncResult 模式比基于事件的模式更适用 的情況有三種: 對 IAsyncResult 阻止等待操作 對多個 IAsyncResult 對象阻止等待操作 對 IAsyncResult 輪詢完成狀态 雖然可以使用基于事件的模式來處理這些情況,但這樣做比使用 IAsyncResult 模式更不友善。 開發人員經常對性能要求通常很高的服務使用 IAsyncResult 模式。 例如,輪詢完成狀态就是一種高性能伺服器技術。 此外,基于事件的模式的效率低于 IAsyncResult 模式,因為前者建立的對象更多(尤其是EventArgs),并且跨線程同步。 下面列出了一些在決定使用 IAsyncResult 模式時要遵循的建議: 僅在特别需要對 WaitHandle 或IAsyncResult 對象的支援時,才公開 IAsyncResult 模式。 僅在有使用 IAsyncResult 模式的現有 API 時,才公開 IAsyncResult 模式。 如果有基于 IAsyncResult 模式的現有 API,還請考慮在下一個版本中公開基于事件的模式。 僅在有高性能要求,且已驗證無法通過基于事件的模式滿足這些要求,但可以通過 IAsyncResult 模式滿足時,才公開 IAsyncResult 模式。
異步程式設計模型的核心是 <code>IAsyncResult</code> 接口,這個接口隻有 <code>IsCompleted</code> 、<code>AsyncWaitHandle</code>、<code>AsyncState</code>、<code>CompletedSynchronously</code> 四個屬性。<code>IAsyncResult</code>的對象存儲異步操作的資訊。
屬性
說明
IsCompleted
異步操作是否完成
AsyncWaitHandle
等待異步完成的句柄(信号量)
AsyncState
使用者自定義對象,可包含上下文或異步操作資訊【可選的】
CompletedSynchronously
異步操作是否【同步】完成(在調用異步的線程上,而不是單獨的線程池)
異步操作通過 <code>BeginOperationName</code> 和 <code>EndOperationName</code> 兩個方法實作,分别開始和結束異步操作。
開始異步操作,使用 <code>BeginOperationName</code> 方法
Begin方法具有同步版本方法 <code>OperationName</code> 的中的所有參數
Begin方法還有另一個參數 <code>AsyncCallback</code> 委托,在異步完成後自動調用,如不希望調用,設定成<code>null</code>
Begin方法還有另一個參數 <code>Object</code> 使用者定義對象,一般即 <code>AsyncState</code>
Begin方法的傳回值是<code>IAsyncResult</code>
Begin方法執行後,無論異步操作是否結束,都立即傳回對調用線程的控制
如果Begin方法引發異常,則會在異步操作之前引發異常,并且不會調用回調方法
結束異步操作,使用 <code>EndOperationName</code> 方法
End 方法用于結束異步操作 <code>OperationName</code>,有一個 <code>IAsyncResult</code> 參數,是Begin 方法的傳回值
End 方法傳回值與<code>OperationName</code>類型相同
End 方法調用時,如果 <code>IAsyncResult</code> 對應的異步操作沒有完成,那麼 End 方法将阻塞
異步操作引發的異常會從 End 方法抛出。重複調用End方法,和End方法使用未傳回的<code>IAsyncResult</code>參數的情況,應考慮引發<code>InvalidOperationException</code>。
異步操作的阻塞,同步執行
異步程式設計模型(APM)中使用阻塞實作程式同步執行有三種方式:調用<code>EndOperationName</code>、使用 <code>IAsyncResult</code> 中的 <code>AsyncWaitHandle</code>、使用時間輪詢<code>IsCompleted</code> 。
使用委托進行異步程式設計
委托有<code>Invoke</code>同步執行方法,和<code>BeginInvoke</code>、<code>EndInvoke</code>異步方法,對同步方法使用委托就可以實作異步程式設計。
舉例說明異步程式設計APM的使用方法。例子中有兩個同步方法<code>ReadBook</code>、<code>ListenMusic</code>。同時使用委托對ReadBook同步方法封裝兩個異步方法<code>BeginReadBook</code>和<code>EndReadBook</code>。同時還包括一個<code>ReadBookFinishCallback</code>回調方法。以此來示範異步程式設計模型(APM)中的常用的内容。
ReadBook同步方法定義
<code>BeginReadBook</code> 和<code>EndReadBook</code>異步方法定義(使用委托封裝)
封裝的 <code>BeginReadBook</code> 和 <code>EndReadBook</code> 異步方法,就是常見的APM異步方法。一般使用此種方式實作異步的架構或者庫都是以這種形式提供。
<code>ListenMusic</code> 同步方法定義
<code>ReadBookFinishCallback</code> 回調函數定義
依次調用 <code>ReadBook</code> 和 <code>ListenMusic</code> 同步方法。
運作結果:同步執行,方法依次調用,讀書結束後再進行聽歌。
示範2:異步調用 + 異步回調
依次調用 <code>BeginReadBook</code> 異步方法和 <code>ListenMusic</code> 同步方法,并且使用回調方法。
運作結果:方法依次調用,異步調用讀書,調用結束後傳回對調用線程(主線程)的控制。繼續調用聽歌的方法。讀書和聽歌同時進行,在聽歌沒有結束的時候,讀書已經完成,觸發<code>ReadBookFinishCallback</code>回調。
示範3: <code>EndReadBook</code> 阻塞實作同步執行
依次調用 <code>BeginReadBoo</code>k 、<code>BeginReadBook</code> 異步方法和 <code>ListenMusic</code>方法 ,<code>BeginReadBook</code>對 <code>ListenMusic</code> 阻塞。
運作結果:在調用 <code>BeginReadBook</code> 異步調用後,<code>EndReadBook</code> 阻塞,<code>ListenMusic</code> 在 <code>EndReadBook</code> 執行結束(異步執行結束)後才執行。
示範4:對<code>EndReadBook</code>重複調用會出現異常
運作結果:這些異常需要開發者及時處理
示範5:輪詢方式阻塞,實作同步執行
示範6:<code>WaitOne</code>阻塞,實作同步執行
基于任務的異步模式(TAP)雖然是新程式設計所推薦的,但也不是萬能的。有些場景使用基于事件的異步模式(EAP)比較合适。異步程式設計模型(APM)用起來不太友好,但是EAP的性能要比APM差,對于性能要求高的服務,用APM要比EAP合适的太多。
異步程式設計模式中的三種方法都有其存在的合理性。在白嫖别人的庫的時候,經常遇到不同的異步操作方式。是以互操作就顯得很重要,我們可以将APM和EAP遷移到TAP,也可以把TAP遷移成APM和EAP來達到相容性。
以 Read 方法為例,其 ATP 的實作如下:
我們使用 <code>TaskFactory<T>.FromAsync</code>方法來實作 TAP 包裝:
這種實作類似以下内容:
如果現有的基礎結構需要 APM 模式,則還需要采用 TAP 實作并在需要 APM 實作的地方使用它。 由于任務可以組合,并且 <code>Task</code>類實作 <code>IAsyncResult</code>,您可以使用一個簡單的 helper 函數執行此操作。 以下代碼使用<code>Task</code> 類的擴充,但可以對非泛型任務使用幾乎相同的函數。
現在,請考慮具有以下 TAP 實作的用例:
并且想要提供此 APM 實作:
以下示例示範了一種向 APM 遷移的方法:
包裝EAP比包裝 APM 模式更為複雜,因為與 APM 模式相比,EAP 模式的變體更多,結構更少。 為了示範,以下代碼包裝了 <code>DownloadStringAsync</code> 方法。 <code>DownloadStringAsync</code> 接受 URI,在下載下傳時引發 <code>DownloadProgressChanged</code> 事件,以報告進度的多個統計資訊,并在完成時引發 <code>DownloadStringCompleted</code> 事件。 最終在指定 URI 中傳回一個字元串,其中包含頁面内容。
雖然等待句柄不能實作異步模式,但進階開發人員可以在設定等待句柄時使用 <code>WaitHandle</code> 類和 <code>ThreadPool.RegisterWaitForSingleObject</code>方法實作異步通知。 可以包裝<code>RegisterWaitForSingleObject</code>方法以在等待句柄中啟用針對任何同步等待的基于任務的替代方法:
使用此方法,可以在異步方法中使用現有 <code>WaitHandle</code> 實作。 例如,若要限制在任何特定時間執行的異步操作數,可以利用信号燈(<code>System.Threading.SemaphoreSlim</code>) 對象)。 可以将并發運作的操作數目限制到 N,方法為:初始化到 N 的信号量的數目、在想要執行操作時等待信号量,并在完成操作時釋放信号量 :
正如前面所述,<code>Task</code> 類實作<code>IAsyncResult</code>,且該實作公開<code>IAsyncResult.AsyncWaitHandle</code>屬性,該屬性會傳回在<code>Task</code>完成時設定的等待句柄。 可以獲得 <code>WaitHandle</code>的<code>Task</code>,如下所示:
示例代碼