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