c# 異步編程基礎(chǔ)講解
現(xiàn)代應(yīng)用程序廣泛使用文件和網(wǎng)絡(luò) I/O。I/O 相關(guān) API 傳統(tǒng)上默認(rèn)是阻塞的,導(dǎo)致用戶體驗和硬件利用率不佳,此類問題的學(xué)習(xí)和編碼的難度也較大。而今基于 Task 的異步 API 和語言級異步編程模式顛覆了傳統(tǒng)模式,使得異步編程非常簡單,幾乎沒有新的概念需要學(xué)習(xí)。
異步代碼有如下特點:
- 在等待 I/O 請求返回的過程中,通過讓出線程來處理更多的服務(wù)器請求。
- 通過在等待 I/O 請求時讓出線程進(jìn)行 UI 交互,并將長期運行的工作過渡到其他 CPU,使用戶界面的響應(yīng)性更強。
- 許多較新的 .NET API 都是異步的。
- 在 .NET 中編寫異步代碼很容易。
使用 .NET 基于 Task 的異步模型可以直接編寫 I/O 和 CPU 受限的異步代碼。該模型圍繞著Task和Task<T>類型以及 C# 的async和await關(guān)鍵字展開。本文將講解如何使用 .NET 異步編程及一些相關(guān)基礎(chǔ)知識。
Task 和 Task<T>
Task 是 Promise 模型的實現(xiàn)。簡單說,它給出“承諾”:會在稍后完成工作。而 .NET 的 Task 是為了簡化使用“承諾”而設(shè)計的 API。
Task 表示不返回值的操作, Task<T> 表示返回T類型的值的操作。
重要的是要把 Task 理解為發(fā)起異步工作的抽象,而不是對線程的抽象。默認(rèn)情況下,Task 在當(dāng)前線程上執(zhí)行,并酌情將工作委托給操作系統(tǒng)??梢赃x擇通過Task.RunAPI 明確要求任務(wù)在單獨的線程上運行。
Task 提供了一個 API 協(xié)議,用于監(jiān)視、等待和訪問任務(wù)的結(jié)果值。比如,通過await關(guān)鍵字等待任務(wù)執(zhí)行完成,為使用 Task 提供了更高層次的抽象。
使用 await 允許你在任務(wù)運行期間執(zhí)行其它有用的工作,將控制權(quán)交給其調(diào)用者,直到任務(wù)完成。你不再需要依賴回調(diào)或事件來在任務(wù)完成后繼續(xù)執(zhí)行后續(xù)工作。
I/O 受限異步操作
下面示例代碼演示了一個典型的異步 I/O 調(diào)用操作:
public Task<string> GetHtmlAsync() { // 此處是同步執(zhí)行 var client = new HttpClient(); return client.GetStringAsync("https://www.dotnetfoundation.org"); }
這個例子調(diào)用了一個異步方法,并返回了一個活動的 Task,它很可能還沒有完成。
下面第二個代碼示例增加了async和await關(guān)鍵字對任務(wù)進(jìn)行操作:
public async Task<string> GetFirstCharactersCountAsync(string url, int count) { // 此處是同步執(zhí)行 var client = new HttpClient(); // 此處 await 掛起代碼的執(zhí)行,把控制權(quán)交出去(線程可以去做別的事情) var page = await client.GetStringAsync("https://www.dotnetfoundation.org"); // 任務(wù)完成后恢復(fù)了控制權(quán),繼續(xù)執(zhí)行后續(xù)代碼 // 此處回到了同步執(zhí)行 if (count > page.Length) { return page; } else { return page.Substring(0, count); } }
使用 await 關(guān)鍵字告訴當(dāng)前上下文趕緊生成快照并交出控制權(quán),異步任務(wù)執(zhí)行完成后會帶著返回值去線程池排隊等待可用線程,等到可用線程后,恢復(fù)上下文,線程繼續(xù)執(zhí)行后續(xù)代碼。
GetStringAsync() 方法的內(nèi)部通過底層 .NET 庫調(diào)用資源(也許會調(diào)用其他異步方法),一直到 P/Invoke 互操作調(diào)用本地(Native)網(wǎng)絡(luò)庫。本地庫隨后可能會調(diào)用到一個系統(tǒng) API(如 Linux 上 Socket 的write()API)。Task 對象將通過層層傳遞,最終返回給初始調(diào)用者。
在整個過程中,關(guān)鍵的一點是,沒有一個線程是專門用來處理任務(wù)的。雖然工作是在某種上下文中執(zhí)行的(操作系統(tǒng)確實要把數(shù)據(jù)傳遞給設(shè)備驅(qū)動程序并中斷響應(yīng)),但沒有線程專門用來等待請求的數(shù)據(jù)回返回。這使得系統(tǒng)可以處理更大的工作量,而不是干等著某個 I/O 調(diào)用完成。
雖然上面的工作看似很多,但與實際 I/O 工作所需的時間相比,簡直微不足道。用一條不太精確的時間線來表示,大概是這樣的:
0-1--------------------2-3
從0到1所花費的時間是await交出控制權(quán)之前所花的時間。從1到2花費的時間是GetStringAsync方法花費在 I/O 上的時間,沒有 CPU 成本。最后,從2到3花費的時間是上下文重新獲取控制權(quán)后繼續(xù)執(zhí)行的時間。
CPU 受限異步操作
CPU 受限的異步代碼與 I/O 受限的異步代碼有些不同。因為工作是在 CPU 上完成的,所以沒有辦法繞開專門的線程來進(jìn)行計算。使用 async 和 await 只是為你提供了一種干凈的方式來與后臺線程進(jìn)行交互。請注意,這并不能為共享數(shù)據(jù)提供加鎖保護(hù),如果你正在使用共享數(shù)據(jù),仍然需要使用適當(dāng)?shù)耐讲呗浴?/p>
下面是一個 CPU 受限的異步調(diào)用:
public async Task<int> CalculateResult(InputData data) { // 在線程池排隊獲取線程來處理任務(wù) var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data)); // 此時此處,你可以并行地處理其它工作 var result = await expensiveResultTask; return result; }
CalculateResult方法在它被調(diào)用的線程(一般可以定義為主線程)上執(zhí)行。當(dāng)它調(diào)用Task.Run時,會在線程池上排隊執(zhí)行 CPU 受限操作 DoExpensiveCalculation,并接收一個Task<int>句柄。DoExpensiveCalculation會在下一個可用的線程上并行運行,很可能是在另一個 CPU 核上。和 I/O 受限異步調(diào)用一樣,一旦遇到await,CalculateResult的控制權(quán)就會被交給它的調(diào)用者,這樣在DoExpensiveCalculation返回結(jié)果的時候,結(jié)果就會被安排在主線程上排隊運行。
對于開發(fā)者,CUP 受限和 I/O 受限的在調(diào)用方式上沒什么區(qū)別。區(qū)別在于所調(diào)用資源性質(zhì)的不同,不必關(guān)心底層對不同資源的調(diào)用的具體邏輯。編寫代碼需要考慮的是,對于 CUP 受限的異步任務(wù),根據(jù)實際情況考慮是否需要使其和其它任務(wù)并行執(zhí)行,以加快程序的整體運行時間。
異步編程模式
最后簡單回顧一下 .NET 歷史上提供的三種執(zhí)行異步操作的模式。
- 基于任務(wù)的異步模式(Task-based Asynchronous Pattern,TAP),它使用單一的方法來表示異步操作的啟動和完成。TAP 是在 .NET Framework 4 中引入的。它是 .NET 中異步編程的推薦方法。C# 中的 async 和 await 關(guān)鍵字為 TAP 添加了語言支持。
- 基于事件的異步模式(Event-based Asynchronous Pattern,EAP),這是基于事件的傳統(tǒng)模式,用于提供異步行為。它需要一個具有 Async 后綴的方法和一個或多個事件。EAP 是在 .NET Framework 2.0 中引入的。它不再被推薦用于新的開發(fā)。
- 異步編程模式(Asynchronous Programming Model,APM)模式,也稱為 IAsyncResult 模式,這是使用 IAsyncResult 接口提供異步行為的傳統(tǒng)模式。在這種模式中,需要Begin和End方法同步操作(例如,BeginWrite和EndWrite來實現(xiàn)異步寫操作)。這種模式也不再推薦用于新的開發(fā)。
下面簡單舉例對三種模式進(jìn)行比較。
假設(shè)有一個 Read 方法,該方法從指定的偏移量開始將指定數(shù)量的數(shù)據(jù)讀入提供的緩沖區(qū):
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 異步模式,則需要定義兩個方法,一個用于開始執(zhí)行異步操作,一個用于接收異步操作結(jié)果:
public class MyClass { public IAsyncResult BeginRead( byte [] buffer, int offset, int count, AsyncCallback callback, object state); public int EndRead(IAsyncResult asyncResult); }
后兩種異步模式已經(jīng)過時不推薦使用了,這里也不再繼續(xù)探討。歲數(shù)大點的 .NET 程序員可能比較熟悉后兩種異步模式,畢竟那時候沒有 async/await,應(yīng)該沒少折騰。
以上就是c# 異步編程基礎(chǔ)講解的詳細(xì)內(nèi)容,更多關(guān)于c# 異步編程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#中的IEnumerable簡介及簡單實現(xiàn)實例
這篇文章主要介紹了C#中的IEnumerable簡介及簡單實現(xiàn)實例,本文講解了IEnumerable一些知識并給出了一個簡單的實現(xiàn),需要的朋友可以參考下2015-03-03SQL+C#實現(xiàn)獲得當(dāng)前月的第一天與最后一天
本文分享了SQL+C#獲得當(dāng)前月的第一天與最后一天的代碼實例,代碼簡潔,適合初學(xué)者參考。需要的朋友可以看下2016-12-12C#使用SignalR實現(xiàn)與前端vue實時通信的示例代碼
SignalR 是 ASP.NET Core 的一個庫,它簡化了在應(yīng)用程序中添加實時通信的過程,無論是聊天應(yīng)用、實時游戲還是協(xié)作工具,SignalR 都能提供高效且易于實現(xiàn)的解決方案,本文給大家介紹了C#使用SignalR實現(xiàn)與前端vue實時通信的實現(xiàn),需要的朋友可以參考下2024-10-10