c# 幾個(gè)常見的TAP異步操作
在本系列上一篇文章 [15:異步編程基礎(chǔ)] 中,我們講到,現(xiàn)代應(yīng)用程序廣泛使用的是基于任務(wù)的異步編程模式(TAP),歷史的 EAP 和 AMP 模式已經(jīng)過時(shí)不推薦使用。今天繼續(xù)總結(jié)一下 TAP 的異步操作,比如取消任務(wù)、報(bào)告進(jìn)度、Task.Yield()、ConfigureAwait() 和并行操作等。
雖然實(shí)際 TAP 編程中很少使用到任務(wù)的狀態(tài),但它是很多 TAP 操作機(jī)理的基礎(chǔ),所以下面先從任務(wù)狀態(tài)講起。
1 任務(wù)狀態(tài)
Task 類為異步操作提供了一個(gè)生命周期,這個(gè)周期由 TaskStatus 枚舉表示,它有如下值:
public enum TaskStatus { Created = 0, WaitingForActivation = 1, WaitingToRun = 2, Running = 3, WaitingForChildrenToComplete = 4, RanToCompletion = 5, Canceled = 6, Faulted = 7 }
其中 Canceled、Faulted 和 RanToCompletion 狀態(tài)一起被認(rèn)為是任務(wù)的最終狀態(tài)。因此,如果任務(wù)處于最終狀態(tài),則其 IsCompleted 屬性為 true 值。
手動控制任務(wù)啟動
為了支持手動控制任務(wù)啟動,并支持構(gòu)造與調(diào)用的分離,Task 類提供了一個(gè) Start 方法。由 Task 構(gòu)造函數(shù)創(chuàng)建的任務(wù)被稱為冷任務(wù),因?yàn)樗鼈兊纳芷谔幱?Created 狀態(tài),只有該實(shí)例的 Start 方法被調(diào)用才會啟動。
任務(wù)狀態(tài)平時(shí)用的情況不多,一般我們在封裝一個(gè)任務(wù)相關(guān)的方法時(shí),可能會用到。比如下面這個(gè)例子,需要判斷某任務(wù)滿足一定條件才啟動:
static void Main(string[] args) { MyTask t = new(() => { // do something. }); StartMyTask(t); Console.ReadKey(); } public static void StartMyTask(MyTask t) { if (t.Status == TaskStatus.Created && t.Counter>10) { t.Start(); } else { // 這里模擬計(jì)數(shù),直到 Counter>10 再執(zhí)行 Start while (t.Counter <= 10) { // Do something t.Counter++; } t.Start(); } } public class MyTask : Task { public MyTask(Action action) : base(action) { } public int Counter { get; set; } }
同樣,TaskStatus.Created 狀態(tài)以外的狀態(tài),我們叫它熱任務(wù),熱任務(wù)一定是被調(diào)用了 Start 方法激活過的。
確保任務(wù)已激活
注意,所有從 TAP 方法返回的任務(wù)都必須被激活,比如下面這樣的代碼:
MyTask task = new(() => { Console.WriteLine("Do something."); }); // 在其它地方調(diào)用 await task;
在 await 之前,任務(wù)沒有執(zhí)行 Task.Start 激活,await 時(shí)程序就會一直等待下去。所以如果一個(gè) TAP 方法內(nèi)部使用 Task 構(gòu)造函數(shù)來實(shí)例化要返回的 Task,那么 TAP 方法必須在返回 Task 對象之前對其調(diào)用 Start。
2 任務(wù)取消
在 TAP 中,取消對于異步方法實(shí)現(xiàn)者和消費(fèi)者來說都是可選的。如果一個(gè)操作允許取消,它就會暴露一個(gè)異步方法的重載,該方法接受一個(gè)取消令牌(CancellationToken 實(shí)例)。按照慣例,參數(shù)被命名為 cancellationToken。例如:
public Task ReadAsync( byte [] buffer, int offset, int count, CancellationToken cancellationToken)
異步操作會監(jiān)控這個(gè)令牌是否有取消請求。如果收到取消請求,它可以選擇取消操作,如下面的示例通過 while 來監(jiān)控令牌的取消請求:
static void Main(string[] args) { CancellationTokenSource source = new(); CancellationToken token = source.Token; var task = DoWork(token); // 實(shí)際情況可能是在稍后的其它線程請求取消 Thread.Sleep(100); source.Cancel(); Console.WriteLine($"取消后任務(wù)返回的狀態(tài):{task.Status}"); Console.ReadKey(); } public static Task DoWork(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { // Do something. Thread.Sleep(1000); return Task.CompletedTask; } return Task.FromCanceled(cancellationToken); }
如果取消請求導(dǎo)致工作提前結(jié)束,甚至還沒有開始就收到請求取消,則 TAP 方法返回一個(gè)以 Canceled 狀態(tài)結(jié)束的任務(wù),它的 IsCompleted 屬性為 true,且不會拋出異常。當(dāng)任務(wù)在 Canceled 狀態(tài)下完成時(shí),任何在該任務(wù)注冊的延續(xù)任務(wù)仍都會被調(diào)用和執(zhí)行,除非指定了諸如 NotOnCanceled 這樣的選項(xiàng)來選擇不延續(xù)。
但是,如果在異步任務(wù)在工作時(shí)收到取消請求,異步操作也可以選擇不立刻結(jié)束,而是等當(dāng)前正在執(zhí)行的工作完成后再結(jié)束,并返回 RanToCompletion 狀態(tài)的任務(wù);也可以終止當(dāng)前工作并強(qiáng)制結(jié)束,根據(jù)實(shí)際業(yè)務(wù)情況和是否生產(chǎn)異常結(jié)果返回 Canceled 或 Faulted 狀態(tài)。
對于不能被取消的業(yè)務(wù)方法,不要提供接受取消令牌的重載,這有助于向調(diào)用者表明目標(biāo)方法是否可以取消。
3 進(jìn)度報(bào)告
幾乎所有異步操作都可以提供進(jìn)度通知,這些通知通常用于用異步操作的進(jìn)度信息更新用戶界面。
在 TAP 中,進(jìn)度是通過 IProgress<T> 接口來處理的,該接口作為一個(gè)參數(shù)傳遞給異步方法。下面是一個(gè)典型的的使用示例:
static void Main(string[] args) { var progress = new Progress<int>(n => { Console.WriteLine($"當(dāng)前進(jìn)度:{n}%"); }); var task = DoWork(progress); Console.ReadKey(); } public static async Task DoWork(IProgress<int> progress) { for (int i = 1; i <= 100; i++) { await Task.Delay(100); if (i % 10 == 0) { progress?.Report(i); }; } }
輸出如下結(jié)果:
當(dāng)前進(jìn)度:10%
當(dāng)前進(jìn)度:20%
當(dāng)前進(jìn)度:30%
當(dāng)前進(jìn)度:40%
當(dāng)前進(jìn)度:50%
當(dāng)前進(jìn)度:60%
當(dāng)前進(jìn)度:70%
當(dāng)前進(jìn)度:80%
當(dāng)前進(jìn)度:90%
當(dāng)前進(jìn)度:100%
IProgress<T> 接口支持不同的進(jìn)度實(shí)現(xiàn),這是由消費(fèi)代碼決定的。例如,消費(fèi)代碼可能只關(guān)心最新的進(jìn)度更新,或者希望緩沖所有更新,或者希望為每個(gè)更新調(diào)用一個(gè)操作,等等。所有這些選項(xiàng)都可以通過使用該接口來實(shí)現(xiàn),并根據(jù)特定消費(fèi)者的需求進(jìn)行定制。例如,如果本文前面的 ReadAsync 方法能夠以當(dāng)前讀取的字節(jié)數(shù)的形式報(bào)告進(jìn)度,那么進(jìn)度回調(diào)可以是一個(gè) IProgress<long> 接口。
public Task ReadAsync( byte[] buffer, int offset, int count, IProgress<long> progress)
再如 FindFilesAsync 方法返回符合特定搜索模式的所有文件列表,進(jìn)度回調(diào)可以提供工作完成的百分比和當(dāng)前部分結(jié)果集,它可以用一個(gè)元組來提供這個(gè)信息。
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync( string pattern, IProgress<Tuple<double, ReadOnlyCollection<List<FileInfo>>>> progress)
或使用 API 特有的數(shù)據(jù)類型:
public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync( string pattern, IProgress<FindFilesProgressInfo> progress)
如果 TAP 的實(shí)現(xiàn)提供了接受 IProgress<T> 參數(shù)的重載,它們必須允許參數(shù)為空,在這種情況下,不會報(bào)告進(jìn)度。IProgress<T> 實(shí)例可以作為獨(dú)立的對象,允許調(diào)用者決定如何以及在哪里處理這些進(jìn)度信息。
4 Task.Yield 讓步
我們先來看一段 Task.Yield() 的代碼:
Task.Run(async () => { for(int i=0; i<10; i++) { await Task.Yield(); ... } });
這里的 Task.Yield() 其實(shí)什么也沒干,它返回的是一個(gè)空任務(wù)。那 await 一個(gè)什么也沒做的空任務(wù)有什么用呢?
我們知道,對計(jì)算機(jī)來說,任務(wù)調(diào)度是根據(jù)一定的優(yōu)先策略來安排線程去執(zhí)行的。如果任務(wù)太多,線程不夠用,任務(wù)就會進(jìn)入排隊(duì)狀態(tài)。而 Yield 的作用就是讓出等待的位置,讓后面排除的任務(wù)先行。它字面上的意思就是讓步,當(dāng)任務(wù)做出讓步時(shí),其它任務(wù)就可以盡快被分配線程去執(zhí)行。舉個(gè)現(xiàn)實(shí)生活中的例子,就像你在排隊(duì)辦理業(yè)務(wù)時(shí),好不容易到你了,但你的事情并不急,自愿讓出位置,讓其他人先辦理,自己假裝臨時(shí)有事到外面溜一圈什么事也沒干又回來重新排隊(duì)。默默地做了一次大善人。
Task.Yield() 方法就是在異步方法中引入一個(gè)讓步點(diǎn)。當(dāng)代碼執(zhí)行到讓步點(diǎn)時(shí),就會讓出控制權(quán),去線程池外面兜一圈什么事也沒干再回來重新排隊(duì)。
5 定制異步任務(wù)后續(xù)操作
我們可以對異步任務(wù)執(zhí)行完成的后續(xù)操作進(jìn)行定制。常見的兩個(gè)方法是 ConfigureAwait 和 ContinueWith。
ConfigureAwait
我們先來看一段 Windows Form 中的代碼:
private void button1_Click(object sender, EventArgs e) { var content = CurlAsync().Result; ... } private async Task<string> CurlAsync() { using (var client = new HttpClient()) { returnawait client.GetStringAsync("http://geekgist.com"); } }
想必大家都知道 CurlAsync().Result 這句代碼在 Windows Form 程序中會造成死鎖。原因是 UI 主線程執(zhí)行到這句代碼時(shí),就開始等待異步任務(wù)的結(jié)果,處于阻塞狀態(tài)。而異步任務(wù)執(zhí)行完后回來準(zhǔn)備找 UI 線程繼續(xù)執(zhí)行后面的代碼時(shí),卻發(fā)現(xiàn) UI 線程一直處于“忙碌”的狀態(tài),沒空搭理回來的異步任務(wù)。這就造成了你等我,我又在等你的尷尬局面。
當(dāng)然,這種死鎖的情況只會在 Winform 和早期的 ASP.NET WebForm 中才會發(fā)生,在 Console 和 Web API 應(yīng)用中不會生產(chǎn)死鎖。
解決辦法很簡單,作為異步方法調(diào)用者,我們只需改用 await 即可:
private async void button1_Click(object sender, EventArgs e) { var content = await CurlAsync(); ... }
在異步方法內(nèi)部,我們也可以調(diào)用任務(wù)的 ConfigureAwait(false) 方法來解決這個(gè)問題。如:
private async Task<string> CurlAsync() { using (var client = new HttpClient()) { returnawait client .GetStringAsync("http://geekgist.com") .ConfigureAwait(false); } }
雖然兩種方法都可行,但如果作為異步方法提供者,比如封裝一個(gè)通用庫時(shí),考慮到難免會有新手開發(fā)者會使用 CurlAsync().Result,為了提高通用庫的容錯(cuò)性,我們就可能需要使用 ConfigureAwait 來做兼容。
ConfigureAwait(false) 的作用是告訴主線程,我要去遠(yuǎn)行了,你去做其它事情吧,不用等我。只要先確保一方不在一直等另一方,就能避免互相等待而造成死鎖的情況。
ContinueWith
ContinueWith 方法很容易理解,就是字面上的意思。作用是在異步任務(wù)執(zhí)行完成后,安排后續(xù)要執(zhí)行的工作。示例代碼:
private void Button1_Click(object sender, EventArgs e) { var backgroundScheduler = TaskScheduler.Default; var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext(); Task.Factory .StartNew(_ => DoBackgroundComputation(), backgroundScheduler) .ContinueWith(_ => UpdateUI(), uiScheduler) .ContinueWith(_ => DoAnotherBackgroundComputation(), backgroundScheduler) .ContinueWith(_ => UpdateUIAgain(), uiScheduler); }
如上,可以一直鏈?zhǔn)降膶懴氯?,任?wù)會按照順序執(zhí)行,一個(gè)執(zhí)行完再繼續(xù)執(zhí)行下一個(gè)。若其中一個(gè)任務(wù)返回的狀態(tài)是 Canceled 時(shí),后續(xù)的任務(wù)也將被取消。這個(gè)方法有好些個(gè)重載,在實(shí)際用到的時(shí)候再查看文檔即可。
6 總結(jié)
本文內(nèi)容都是相對比較基礎(chǔ)的 TAP 異步操作知識點(diǎn)。C# 的 TAP 很強(qiáng)大,提供的 API 也很多,遠(yuǎn)不止本文講的這些,都是圍繞 Task 轉(zhuǎn)的。關(guān)鍵是要理解好基礎(chǔ)操作,才能靈活使用更高級的功能。希望本文對你有所幫助。
以上就是c# 幾個(gè)常見的TAP異步操作的詳細(xì)內(nèi)容,更多關(guān)于c# TAP異步操作的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#中GraphicsPath的Warp方法用法實(shí)例
這篇文章主要介紹了C#中GraphicsPath的Warp方法用法,實(shí)例分析了Warp方法的相關(guān)使用技巧,需要的朋友可以參考下2015-06-06C#實(shí)現(xiàn)獲取枚舉中元素個(gè)數(shù)的方法
這篇文章主要介紹了C#實(shí)現(xiàn)獲取枚舉中元素個(gè)數(shù)的方法,是深入理解C#程序設(shè)計(jì)所需要掌握的基本技巧,需要的朋友可以參考下2014-08-08利用C#實(shí)現(xiàn)批量圖片格式轉(zhuǎn)換功能
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)批量圖片格式轉(zhuǎn)換功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-12-12探討Object轉(zhuǎn)為String的幾種簡易形式詳解
本篇文章是對Object轉(zhuǎn)為String的幾種簡易形式進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06淺析C#中數(shù)組,ArrayList與List對象的區(qū)別
在C#中,當(dāng)我們想要存儲一組對象的時(shí)候,就會想到用數(shù)組,ArrayList,List這三個(gè)對象了。那么這三者到底有什么樣的區(qū)別呢2013-07-07C#中BitmapImage與BitmapSource接口的區(qū)別對比小結(jié)
BitmapImage和BitmapSource都可以用于表示和顯示圖像,本文就來介紹一下C#中BitmapImage與BitmapSource接口的區(qū)別對比,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03System.Data.OleDb.OleDbException: 未指定的錯(cuò)誤的完美解決方法
本文給大家?guī)砣N有關(guān)System.Data.OleDb.OleDbException: 未指定的錯(cuò)誤的完美解決方法,每種方法都很不錯(cuò),需要的朋友可以參考下2016-09-09C#調(diào)用和實(shí)現(xiàn)WebService,純手工打造!
C#調(diào)用和實(shí)現(xiàn)WebService,純手工打造! 需要的朋友可以參考一下2013-02-02