[C#.NET 拾遺補(bǔ)漏]15:異步編程基礎(chǔ)

現(xiàn)代應(yīng)用程序廣泛使用文件和網(wǎng)絡(luò) I/O。I/O 相關(guān) API 傳統(tǒng)上默認(rèn)是阻塞的,導(dǎo)致用戶體驗(yàn)和硬件利用率不佳,此類問(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 中編寫異步代碼很容易。
使用 .NET 基于 Task 的異步模型可以直接編寫 I/O 和 CPU 受限的異步代碼。該模型圍繞著Task和Task<T>類型以及 C# 的async和await關(guān)鍵字展開。本文將講解如何使用 .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類型的值的操作。
重要的是要把 Task 理解為發(fā)起異步工作的抽象,而不是對(duì)線程的抽象。默認(rèn)情況下,Task 在當(dāng)前線程上執(zhí)行,并酌情將工作委托給操作系統(tǒng)。可以選擇通過(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è)線程是專門用來(lái)處理任務(wù)的。雖然工作是在某種上下文中執(zhí)行的(操作系統(tǒng)確實(shí)要把數(shù)據(jù)傳遞給設(shè)備驅(qū)動(dòng)程序并中斷響應(yīng)),但沒(méi)有線程專門用來(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)有辦法繞開專門的線程來(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ì)于開發(fā)者,CUP 受限和 I/O 受限的在調(diào)用方式上沒(méi)什么區(qū)別。區(qū)別在于所調(diào)用資源性質(zhì)的不同,不必關(guān)心底層對(duì)不同資源的調(diào)用的具體邏輯。編寫代碼需要考慮的是,對(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 中引入的。它不再被推薦用于新的開發(fā)。異步編程模式(Asynchronous Programming Model,APM)模式,也稱為 IAsyncResult 模式,這是使用 IAsyncResult 接口提供異步行為的傳統(tǒng)模式。在這種模式中,需要
Begin和End方法同步操作(例如,BeginWrite和EndWrite來(lái)實(shí)現(xiàn)異步寫操作)。這種模式也不再推薦用于新的開發(fā)。
下面簡(jiǎn)單舉例對(duì)三種模式進(jìn)行比較。
假設(shè)有一個(gè) Read 方法,該方法從指定的偏移量開始將指定數(shù)量的數(shù)據(jù)讀入提供的緩沖區(qū):
public class MyClass
{
public int Read(byte [] buffer, int offset, int count);
}
若用 TAP 異步模式來(lái)改寫,該方法將是簡(jiǎn)單的一個(gè) 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 異步模式,則需要定義兩個(gè)方法,一個(gè)用于開始執(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)少折騰。
【推薦】.NET Core開發(fā)實(shí)戰(zhàn)視頻課程 ★★★
.NET Core實(shí)戰(zhàn)項(xiàng)目之CMS 第一章 入門篇-開篇及總體規(guī)劃
【.NET Core微服務(wù)實(shí)戰(zhàn)-統(tǒng)一身份認(rèn)證】開篇及目錄索引
Redis基本使用及百億數(shù)據(jù)量中的使用技巧分享(附視頻地址及觀看指南)
.NET Core中的一個(gè)接口多種實(shí)現(xiàn)的依賴注入與動(dòng)態(tài)選擇看這篇就夠了
10個(gè)小技巧助您寫出高性能的ASP.NET Core代碼
用abp vNext快速開發(fā)Quartz.NET定時(shí)任務(wù)管理界面
在ASP.NET Core中創(chuàng)建基于Quartz.NET托管服務(wù)輕松實(shí)現(xiàn)作業(yè)調(diào)度
現(xiàn)身說(shuō)法:實(shí)際業(yè)務(wù)出發(fā)分析百億數(shù)據(jù)量下的多表查詢優(yōu)化
