c# 異步編程基礎講解
現代應用程序廣泛使用文件和網絡 I/O。I/O 相關 API 傳統上默認是阻塞的,導致用戶體驗和硬件利用率不佳,此類問題的學習和編碼的難度也較大。而今基於 Task 的異步 API 和語言級異步編程模式顛覆瞭傳統模式,使得異步編程非常簡單,幾乎沒有新的概念需要學習。
異步代碼有如下特點:
- 在等待 I/O 請求返回的過程中,通過讓出線程來處理更多的服務器請求。
- 通過在等待 I/O 請求時讓出線程進行 UI 交互,並將長期運行的工作過渡到其他 CPU,使用戶界面的響應性更強。
- 許多較新的 .NET API 都是異步的。
- 在 .NET 中編寫異步代碼很容易。
使用 .NET 基於 Task 的異步模型可以直接編寫 I/O 和 CPU 受限的異步代碼。該模型圍繞著Task和Task<T>類型以及 C# 的async和await關鍵字展開。本文將講解如何使用 .NET 異步編程及一些相關基礎知識。
Task 和 Task<T>
Task 是 Promise 模型的實現。簡單說,它給出“承諾”:會在稍後完成工作。而 .NET 的 Task 是為瞭簡化使用“承諾”而設計的 API。
Task 表示不返回值的操作, Task<T> 表示返回T類型的值的操作。
重要的是要把 Task 理解為發起異步工作的抽象,而不是對線程的抽象。默認情況下,Task 在當前線程上執行,並酌情將工作委托給操作系統。可以選擇通過Task.RunAPI 明確要求任務在單獨的線程上運行。
Task 提供瞭一個 API 協議,用於監視、等待和訪問任務的結果值。比如,通過await關鍵字等待任務執行完成,為使用 Task 提供瞭更高層次的抽象。
使用 await 允許你在任務運行期間執行其它有用的工作,將控制權交給其調用者,直到任務完成。你不再需要依賴回調或事件來在任務完成後繼續執行後續工作。
I/O 受限異步操作
下面示例代碼演示瞭一個典型的異步 I/O 調用操作:
public Task<string> GetHtmlAsync() { // 此處是同步執行 var client = new HttpClient(); return client.GetStringAsync("https://www.dotnetfoundation.org"); }
這個例子調用瞭一個異步方法,並返回瞭一個活動的 Task,它很可能還沒有完成。
下面第二個代碼示例增加瞭async和await關鍵字對任務進行操作:
public async Task<string> GetFirstCharactersCountAsync(string url, int count) { // 此處是同步執行 var client = new HttpClient(); // 此處 await 掛起代碼的執行,把控制權交出去(線程可以去做別的事情) var page = await client.GetStringAsync("https://www.dotnetfoundation.org"); // 任務完成後恢復瞭控制權,繼續執行後續代碼 // 此處回到瞭同步執行 if (count > page.Length) { return page; } else { return page.Substring(0, count); } }
使用 await 關鍵字告訴當前上下文趕緊生成快照並交出控制權,異步任務執行完成後會帶著返回值去線程池排隊等待可用線程,等到可用線程後,恢復上下文,線程繼續執行後續代碼。
GetStringAsync() 方法的內部通過底層 .NET 庫調用資源(也許會調用其他異步方法),一直到 P/Invoke 互操作調用本地(Native)網絡庫。本地庫隨後可能會調用到一個系統 API(如 Linux 上 Socket 的write()API)。Task 對象將通過層層傳遞,最終返回給初始調用者。
在整個過程中,關鍵的一點是,沒有一個線程是專門用來處理任務的。雖然工作是在某種上下文中執行的(操作系統確實要把數據傳遞給設備驅動程序並中斷響應),但沒有線程專門用來等待請求的數據回返回。這使得系統可以處理更大的工作量,而不是幹等著某個 I/O 調用完成。
雖然上面的工作看似很多,但與實際 I/O 工作所需的時間相比,簡直微不足道。用一條不太精確的時間線來表示,大概是這樣的:
0-1——————–2-3
從0到1所花費的時間是await交出控制權之前所花的時間。從1到2花費的時間是GetStringAsync方法花費在 I/O 上的時間,沒有 CPU 成本。最後,從2到3花費的時間是上下文重新獲取控制權後繼續執行的時間。
CPU 受限異步操作
CPU 受限的異步代碼與 I/O 受限的異步代碼有些不同。因為工作是在 CPU 上完成的,所以沒有辦法繞開專門的線程來進行計算。使用 async 和 await 隻是為你提供瞭一種幹凈的方式來與後臺線程進行交互。請註意,這並不能為共享數據提供加鎖保護,如果你正在使用共享數據,仍然需要使用適當的同步策略。
下面是一個 CPU 受限的異步調用:
public async Task<int> CalculateResult(InputData data) { // 在線程池排隊獲取線程來處理任務 var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data)); // 此時此處,你可以並行地處理其它工作 var result = await expensiveResultTask; return result; }
CalculateResult方法在它被調用的線程(一般可以定義為主線程)上執行。當它調用Task.Run時,會在線程池上排隊執行 CPU 受限操作 DoExpensiveCalculation,並接收一個Task<int>句柄。DoExpensiveCalculation會在下一個可用的線程上並行運行,很可能是在另一個 CPU 核上。和 I/O 受限異步調用一樣,一旦遇到await,CalculateResult的控制權就會被交給它的調用者,這樣在DoExpensiveCalculation返回結果的時候,結果就會被安排在主線程上排隊運行。
對於開發者,CUP 受限和 I/O 受限的在調用方式上沒什麼區別。區別在於所調用資源性質的不同,不必關心底層對不同資源的調用的具體邏輯。編寫代碼需要考慮的是,對於 CUP 受限的異步任務,根據實際情況考慮是否需要使其和其它任務並行執行,以加快程序的整體運行時間。
異步編程模式
最後簡單回顧一下 .NET 歷史上提供的三種執行異步操作的模式。
- 基於任務的異步模式(Task-based Asynchronous Pattern,TAP),它使用單一的方法來表示異步操作的啟動和完成。TAP 是在 .NET Framework 4 中引入的。它是 .NET 中異步編程的推薦方法。C# 中的 async 和 await 關鍵字為 TAP 添加瞭語言支持。
- 基於事件的異步模式(Event-based Asynchronous Pattern,EAP),這是基於事件的傳統模式,用於提供異步行為。它需要一個具有 Async 後綴的方法和一個或多個事件。EAP 是在 .NET Framework 2.0 中引入的。它不再被推薦用於新的開發。
- 異步編程模式(Asynchronous Programming Model,APM)模式,也稱為 IAsyncResult 模式,這是使用 IAsyncResult 接口提供異步行為的傳統模式。在這種模式中,需要Begin和End方法同步操作(例如,BeginWrite和EndWrite來實現異步寫操作)。這種模式也不再推薦用於新的開發。
下面簡單舉例對三種模式進行比較。
假設有一個 Read 方法,該方法從指定的偏移量開始將指定數量的數據讀入提供的緩沖區:
public class MyClass { public int Read(byte [] buffer, int offset, int count); }
若用 TAP 異步模式來改寫,該方法將是簡單的一個 ReadAsync 方法:
public class MyClass { public Task<int> ReadAsync(byte [] buffer, int offset, int count); }
若使用 EAP 異步模式,需要額外多定義一些類型和成員:
public class MyClass { public void ReadAsync(byte [] buffer, int offset, int count); public event ReadCompletedEventHandler ReadCompleted; } public delegate void ReadCompletedEventHandler( object sender, ReadCompletedEventArgs e); public class ReadCompletedEventArgs : AsyncCompletedEventArgs { public MyReturnType Result { get; } }
若使用 AMP 異步模式,則需要定義兩個方法,一個用於開始執行異步操作,一個用於接收異步操作結果:
public class MyClass { public IAsyncResult BeginRead( byte [] buffer, int offset, int count, AsyncCallback callback, object state); public int EndRead(IAsyncResult asyncResult); }
後兩種異步模式已經過時不推薦使用瞭,這裡也不再繼續探討。歲數大點的 .NET 程序員可能比較熟悉後兩種異步模式,畢竟那時候沒有 async/await,應該沒少折騰。
以上就是c# 異步編程基礎講解的詳細內容,更多關於c# 異步編程的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- None Found