C#并發(fā)編程之a(chǎn)sync和await關(guān)鍵字詳解
〇、前言
對(duì)于 async 和 await 兩個(gè)關(guān)鍵字,對(duì)于一線開發(fā)人員再熟悉不過(guò)了,到處都是它們的身影。
從 C# 5.0 時(shí)代引入 async 和 await 關(guān)鍵字,我們使用 async 修飾符可將方法、lambda 表達(dá)式或匿名方法指定為異步。 如果對(duì)方法或表達(dá)式使用此修飾符,則其稱為異步方法。async 和 await 通過(guò)與 .NET Framework 4.0 時(shí)引入的任務(wù)并行庫(kù)(TPL:Task Parallel Library)構(gòu)成了新的異步編程模型,即 TAP(基于任務(wù)的異步模式 Task-based asynchronous pattern)。
但是如果對(duì)他們不太了解的話,會(huì)有很多麻煩出現(xiàn),所以最近查了一些資料,也看了幾個(gè)大佬的介紹,今天來(lái)記錄匯總下。
一、先通過(guò)一個(gè)簡(jiǎn)單的示例來(lái)互相認(rèn)識(shí)下
如下代碼,在 Main 方法中,調(diào)用一個(gè)異步方法,因?yàn)?Main 本身不支持 async,所以不能直接使用 await 關(guān)鍵字來(lái)完成異步等待等操作。
static void Main(string[] args) // 由于 Main 方法不支持 async,所以只能通過(guò) AsyncTask() 來(lái)調(diào)用異步方法 { Console.WriteLine("--開始!"); Console.WriteLine($"--下面我(主線程)先通知下兒子(子線程)也開始。 我的 ID:{Thread.CurrentThread.ManagedThreadId}"); // 調(diào)用 async 修飾的方法,也就是異步執(zhí)行的方法 AsyncTask(); // 異步方法,不占用主線程,是另新創(chuàng)建的新的子線程 Console.WriteLine("--我(主線程)已經(jīng)讓我兒子(子線程)開始工作了,我也繼續(xù)工作"); Console.WriteLine($"--我(主線程)完成! 我的 ID:{Thread.CurrentThread.ManagedThreadId}"); Console.ReadLine(); } // async 修飾的方法,也就是異步方法,不占用主線程 public static async Task AsyncTask() { Thread.Sleep(1000); Console.WriteLine($"--我剛到,還沒找到兒子(子線程)的房間,我的 ID:{Thread.CurrentThread.ManagedThreadId}"); var result = await WasteTime(); // 主線程遇到 await,是不會(huì)等待的,直接繼續(xù)執(zhí)行,接下來(lái)的事情交給子線程 Console.WriteLine(result); Console.WriteLine($"兒子(子線程)已經(jīng)干完了應(yīng)該干的事情! 我的 ID:{Thread.CurrentThread.ManagedThreadId}"); } // async 修飾的方法,也就是異步方法,不占用主線程 private static async Task<string> WasteTime() { Console.WriteLine($"--我終于找到了,下面準(zhǔn)備讓兒子(子線程)開干!我的 ID:{Thread.CurrentThread.ManagedThreadId}"); return await Task.Run(() => // 創(chuàng)建一個(gè)子線程 { Console.WriteLine($"兒子(子線程)開始異步執(zhí)行了! 我的 ID:{Thread.CurrentThread.ManagedThreadId}"); // 模擬耗時(shí)操作 Thread.Sleep(5000); return $"兒子(子線程)異步執(zhí)行完了。我的 ID:{Thread.CurrentThread.ManagedThreadId}"; }); }
如下結(jié)果輸出,加了雙橫杠--的是主線程的輸出:
二、關(guān)于 async 關(guān)鍵字
使用 async 修飾符可將方法、lambda 表達(dá)式或匿名方法指定為異步,此時(shí) async 稱為關(guān)鍵字,其他所有上下文中都解釋為標(biāo)識(shí)符。如果對(duì)方法或表達(dá)式使用此修飾符,則其稱為異步方法。如下代碼,定義一個(gè)異步方法 ExampleMethodAsync():
public async Task<int> ExampleMethodAsync() { //... }
異步方法同步運(yùn)行,直至到達(dá)其第一個(gè) await 表達(dá)式,此時(shí)會(huì)將方法掛起,直到等待的任務(wù)完成。
如果 async 關(guān)鍵字修改的方法不包含 await 表達(dá)式或語(yǔ)句,則該方法將同步執(zhí)行。編譯器警告將通知你不包含 await 語(yǔ)句的任何異步方法,因?yàn)樵撉闆r可能表示存在錯(cuò)誤。警告信息如下圖:
異步方法可具有以下返回類型:
- Task
- Task<TResult>
- void。 對(duì)于除事件處理程序以外的代碼,通常不鼓勵(lì)使用 async void 方法,因?yàn)檎{(diào)用方不能 await 那些方法,并且必須實(shí)現(xiàn)不同的機(jī)制來(lái)報(bào)告成功完成或錯(cuò)誤條件。
- 任何具有可訪問(wèn)的 GetAwaiter 方法的類型。 System.Threading.Tasks.ValueTask<TResult> 類型屬于此類實(shí)現(xiàn)。 它通過(guò)添加 NuGet 包 System.Threading.Tasks.Extensions 的方式可用。
此異步方法既不能聲明任何 in、ref 或 out 參數(shù),也不能具有引用返回值,但它可以調(diào)用具有此類參數(shù)的方法。
三、關(guān)于 await 關(guān)鍵字
3.1 await 的用法示例
await 運(yùn)算符(異步等待任務(wù)完成)可以讓主線程,跳過(guò)對(duì)其所修飾的 async 方法的執(zhí)行等待,將耗時(shí)操作交給子線程,從而完成異步操作。異步操作完成后,await 運(yùn)算符將返回操作的結(jié)果(如果有)。
當(dāng) await 運(yùn)算符用到表示已完成操作的異步方法時(shí),它將立即返回操作的結(jié)果,類似于同步執(zhí)行。
await 運(yùn)算符不會(huì)阻止計(jì)算異步方法的線程。當(dāng) await 運(yùn)算符占用子線程執(zhí)行其異步方法時(shí),主線程將返回到原執(zhí)行路徑上繼續(xù)往下執(zhí)行。
如下代碼,兩個(gè) async 修飾的異步方法:
- 首先在【1】位置調(diào)用異步方法
DownloadDocsMainPageAsync()
,由于這里沒有 await 運(yùn)算符,所以按照同步方式運(yùn)行,進(jìn)入到方法體內(nèi)部,到達(dá)【2】位置。 - 在【2】位置,代碼中通過(guò)在異步方法
GetByteArrayAsync()
前加了 await 運(yùn)算符,預(yù)示著這里將進(jìn)行異步操作,創(chuàng)建新的線程,然后釋放主線程,繼續(xù)回Main()
函數(shù)中往下運(yùn)行。 - 由于【2】這一行代碼是耗時(shí)操作,因此主線程執(zhí)行到【3】位置,這里有出現(xiàn)了 await 運(yùn)算符,指的是等待異步線程的結(jié)果,此時(shí)主線程就下線了,接下來(lái)就是子線程的表演時(shí)間了。
- 最后子線程下載操作完成,返回到【3】位置,完成其余的工作。
public static async Task Main() { Task<int> downloading = DownloadDocsMainPageAsync(); // 【1】 Console.WriteLine($"{nameof(Main)}: 啟動(dòng)下載。。。ThreadID:{Thread.CurrentThread.ManagedThreadId}"); int bytesLoaded = await downloading; // 【3】 Console.WriteLine($"{nameof(Main)}: 共下載了 {bytesLoaded} bytes。ThreadID:{Thread.CurrentThread.ManagedThreadId}"); Console.ReadLine(); } private static async Task<int> DownloadDocsMainPageAsync() { Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: 即將開始下載。ThreadID:{Thread.CurrentThread.ManagedThreadId}"); var client = new HttpClient(); byte[] content = await client.GetByteArrayAsync("https://learn.microsoft.com/en-us/"); // 【2】 Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: 完成下載。ThreadID:{Thread.CurrentThread.ManagedThreadId}"); return content.Length; }
輸出結(jié)果如下圖:
代碼實(shí)際執(zhí)行的流程大概畫下:
3.2 await foreach() 示例
可以通過(guò) await foreach 語(yǔ)句來(lái)使用異步數(shù)據(jù)流,即實(shí)現(xiàn) IAsyncEnumerable<T> 接口的集合類型。異步檢索下一個(gè)元素時(shí),可能會(huì)掛起循環(huán)的每次迭代。
public class Program { static async Task Main(string[] args) { const int count = 5; ConsoleExt.WriteLineAsync($"-------------------1開始示例異步測(cè)試"); //ConsoleExt.WriteLineAsync($"-------------------2開始示例異步測(cè)試"); //ConsoleExt.WriteLineAsync($"-------------------3開始示例異步測(cè)試"); // 創(chuàng)建一個(gè)新的任務(wù),用于【生成】異步序列數(shù)據(jù) IAsyncEnumerable<int> pullBasedAsyncSequence = ProduceAsyncSumSeqeunc(count); // 創(chuàng)建一個(gè)新的任務(wù),用于【使用】異步序列數(shù)據(jù) var consumingTask = Task.Run(() => ConsumeAsyncSumSeqeunc(pullBasedAsyncSequence)); ConsoleExt.WriteLineAsync($"-------------------開始做其他耗時(shí)操作"); await Task.Delay(TimeSpan.FromSeconds(3)); // 模擬耗時(shí)操作 ConsoleExt.WriteLineAsync($"-------------------結(jié)束做其他耗時(shí)操作"); await consumingTask; // 等待異步任務(wù)完成 ConsoleExt.WriteLineAsync($"-------------------結(jié)束示例異步測(cè)試"); Console.ReadLine(); } static async Task ConsumeAsyncSumSeqeunc(IAsyncEnumerable<int> sequence) // 使用 { ConsoleExt.WriteLineAsync($"ConsumeAsyncSumSeqeunc 被調(diào)用"); await foreach (var value in sequence) { ConsoleExt.WriteLineAsync($"----接收延遲返回的值 {value}"); await Task.Delay(TimeSpan.FromSeconds(1)); // 模擬耗時(shí)操作 }; } private static async IAsyncEnumerable<int> ProduceAsyncSumSeqeunc(int count) // 生成 { ConsoleExt.WriteLineAsync($"ProduceAsyncSumSeqeunc 被調(diào)用"); var sum = 0; for (var i = 0; i <= count; i++) { sum = sum + i; await Task.Delay(TimeSpan.FromSeconds(0.5)); // 模擬耗時(shí)操作 ConsoleExt.WriteLineAsync($"ProduceAsyncSumSeqeunc 返回 sum:{sum}"); yield return sum; // yield 關(guān)鍵字表示延遲加載,將全部返回值一個(gè)一個(gè)返回 } } } public static class ConsoleExt { public static void WriteLine(object message) { Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "); } public static async void WriteLineAsync(object message) { await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ")); } }
輸出結(jié)果如下圖,特別關(guān)注一下線程 12,它不僅在 foreach 迭代中執(zhí)行任務(wù),而且還抽空把Main()方法中的也執(zhí)行了,這樣就極大的發(fā)揮了多線程的好處,任務(wù)操作安排的滿滿的,避免浪費(fèi)資源。
常規(guī)的 foreach() 方法,是單線程的,后一個(gè)操作必須在前一個(gè)操作完成后開始,這樣對(duì)于多邏輯處理器的機(jī)器來(lái)說(shuō),就像是宰牛刀對(duì)付小雞兒了。
詳情可參考:聊一聊C# 8.0中的await foreach
3.3 關(guān)于 await using()
可以說(shuō) await using() 的使用是和 IAsyncDisposable 接口息息相關(guān)的。
IAsyncDisposable 接口,提供一種用于異步釋放非托管資源的機(jī)制。與之對(duì)應(yīng)的就是提供同步釋放非托管資源機(jī)制的接口 IDisposable。提供此類及時(shí)釋放機(jī)制,可使用戶執(zhí)行資源密集型釋放操作,從而無(wú)需長(zhǎng)時(shí)間占用 GUI 應(yīng)用程序的主線程。同時(shí)更好的完善.NET異步編程的體驗(yàn),IAsyncDisposable誕生了。
現(xiàn)在 .NET 的很多類庫(kù)都已經(jīng)同時(shí)支持了 IDisposable 和 IAsyncDisposable。而從使用者的角度來(lái)看,其實(shí)調(diào)用任何一個(gè)釋放方法都能夠達(dá)到釋放資源的目的。就好比 DbContext 的 SaveChanges和 SaveChangesAsync。但是從未來(lái)的發(fā)展角度來(lái)看,IAsyncDisposable 會(huì)成使用的更加頻繁。因?yàn)樗鼞?yīng)該能夠優(yōu)雅地處理托管資源,而不必?fù)?dān)心死鎖。而對(duì)于現(xiàn)在已有代碼中實(shí)現(xiàn)了 IDisposable 的類,如果想要使用 IAsyncDisposable。建議您同時(shí)實(shí)現(xiàn)兩個(gè)接口,已保證使用者在使用時(shí),無(wú)論調(diào)用哪個(gè)接口都能達(dá)到效果,而達(dá)到兼容性的目的。
如下示例代碼繼承了 IAsyncDisposable 接口,然后就可以使用 await using 語(yǔ)法了:
// 【前提】先實(shí)現(xiàn)接口 IAsyncDisposable public class ExampleClass : IAsyncDisposable { private Stream _memoryStream = new MemoryStream(); public ExampleClass() { } public async ValueTask DisposeAsync() { await _memoryStream.DisposeAsync(); } } // 【第一種】然后就可以使用 using 語(yǔ)法糖 await using var s = new ExampleClass() { // 具體操作。。。 }; // 【第二種】?jī)?yōu)化 同樣是對(duì)象 s 只存在于當(dāng)前代碼塊 await using var s = new ExampleClass(); // 具體操作。。。
詳情可參考:熟悉而陌生的新朋友——IAsyncDisposable
四、await Task 和 Task.GetAwaiter()
4.1 關(guān)于 Task.GetAwaiter()
最常用的等待異步線程完成的修飾符就是 await,那么如果不用它怎么判斷任務(wù)執(zhí)行情況呢?這時(shí)候 Task.GetAwaiter() 就上場(chǎng)了。
如下代碼,task.GetAwaiter().OnCompleted(() => { })
的目的就是在 task 執(zhí)行狀態(tài)為 RunToCompletion 時(shí)執(zhí)行其中的匿名函數(shù)。
class Program { static void Main() { var task = Task.Run(() => { return GetName(); }); task.GetAwaiter().OnCompleted(() => { var name = task.Result; ConsoleExt.WriteLine("獲取到的名稱為:" + name); }); ConsoleExt.WriteLine("主線程執(zhí)行完畢"); Console.ReadLine(); } static string GetName() { ConsoleExt.WriteLine("另外一個(gè)線程在獲取名稱"); Thread.Sleep(2000); return "GetName--名稱"; } } public static class ConsoleExt { public static void WriteLine(object message) { Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} "); } public static async void WriteLineAsync(object message) { await Task.Run(() => Console.WriteLine($"(Time: {DateTime.Now.ToString("HH:mm:ss.ffffff")}, Thread {Thread.CurrentThread.ManagedThreadId}): {message} ")); } }
如下輸出結(jié)果,1 為主線程,4 為子線程:
4.2 await Task 和 Task.GetAwaiter() 的區(qū)別
在異步返回的 Task 實(shí)例前加上 await 關(guān)鍵字之后,后面的代碼會(huì)被掛起等待,直到 task 執(zhí)行完畢有返回值的時(shí)候才會(huì)繼續(xù)向下執(zhí)行,這一段時(shí)間主線程會(huì)處于掛起狀態(tài)。例如本文 3.1 await 的用法示例 中的示例,總共下載了多少內(nèi)容在最后才被輸出。
GetAwaiter() 方法則會(huì)返回一個(gè) awaitable 的對(duì)象(繼承了 INotifyCompletion.OnCompleted 方法),通過(guò)public void OnCompleted(Action continuation)
方法,我們只是傳遞了一個(gè)委托(Action)進(jìn)去,等 task 完成了就會(huì)執(zhí)行這個(gè)委托,但是并不會(huì)影響主線程,下面的代碼會(huì)立即執(zhí)行。這也是為什么我們?cè)诒疚纳弦徽鹿?jié) 4.1 關(guān)于 Task.GetAwaiter() 的輸出結(jié)果里面,“主線程執(zhí)行完畢”寫在最后,而非最后輸出的原因。
那么我們通過(guò) GetAwaiter() 方法如何能達(dá)到 await Task 的效果呢?
// GetResult() 方法就是阻塞線程,直到 task 執(zhí)行完成,返回結(jié)果 name var name = task.GetAwaiter().GetResult(); // 上邊這行的效果,等同于 var name = await task;
await 實(shí)質(zhì)是在調(diào)用 awaitable 對(duì)象的 GetResult() 方法。
以上就是C#并發(fā)編程之a(chǎn)sync和await關(guān)鍵字詳解的詳細(xì)內(nèi)容,更多關(guān)于C# async await的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#實(shí)現(xiàn)從多列的DataTable里取需要的幾列
這篇文章主要介紹了C#實(shí)現(xiàn)從多列的DataTable里取需要的幾列,涉及C#針對(duì)DataTable操作的相關(guān)技巧,需要的朋友可以參考下2016-03-03C#實(shí)現(xiàn)打印與打印預(yù)覽功能的思路及代碼
這篇文章主要介紹了C#實(shí)現(xiàn)打印與打印預(yù)覽功能的思路及代碼,有需要的朋友可以參考一下2013-12-12C#實(shí)現(xiàn)通過(guò)程序自動(dòng)抓取遠(yuǎn)程Web網(wǎng)頁(yè)信息的代碼
C#實(shí)現(xiàn)通過(guò)程序自動(dòng)抓取遠(yuǎn)程Web網(wǎng)頁(yè)信息的代碼...2007-04-04C#實(shí)現(xiàn)將文件轉(zhuǎn)換為XML的方法
這篇文章主要介紹了C#實(shí)現(xiàn)將文件轉(zhuǎn)換為XML的方法,實(shí)例分析了office文件與xml的相互轉(zhuǎn)換技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-12-12利用C#實(shí)現(xiàn)HTML模板的循環(huán)輸出
模板循環(huán)輸出 ,是指使用 UI 前端設(shè)計(jì)的 HTML 模板片斷,并結(jié)合數(shù)據(jù)記錄進(jìn)行循環(huán)輸出的過(guò)程,本文將介紹如何中通過(guò) C# 實(shí)現(xiàn)操作 HTML 模板的循環(huán)輸出,文章通過(guò)代碼示例講解的非常詳細(xì),需要的朋友可以參考下2024-06-06C#使用stackalloc分配堆棧內(nèi)存和非托管類型詳解
這篇文章主要為大家介紹了C#使用stackalloc分配堆棧內(nèi)存和非托管類型詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-12-12