C# async/await任務(wù)超時(shí)處理的實(shí)現(xiàn)
一、需求
在之前的帖子中,介紹了 async / await 的用法,那么新的問(wèn)題又來(lái)了,如果調(diào)用一個(gè)異步方法后,一直不給返回值結(jié)果怎么辦呢?這就涉及到怎么取消任務(wù)了。添加一個(gè)任務(wù)后,如果固定時(shí)間內(nèi)沒用返回結(jié)果,那么就取消執(zhí)行,并且在多個(gè)任務(wù)同時(shí)執(zhí)行的時(shí)候,依然按順序來(lái)執(zhí)行,這就是本文章要實(shí)現(xiàn)的功能。
二、Task取消任務(wù)
先介紹一下 Task 結(jié)束任務(wù)的傳統(tǒng)用法。在 C# 以前的2.0 等版本中,線程是可以強(qiáng)制終止的,到了后面,就不允許強(qiáng)制去結(jié)束線程的,后面微軟就提供了一個(gè) CancellationTokenSource 相關(guān)的接口開源取消任務(wù)。于是我搜索了大量的帖子,一看全是各種抄襲,相互抄來(lái)抄去的,搞的我真的火冒三丈,完全是浪費(fèi)時(shí)間,那么總結(jié)取消的方法如下:
namespace 取消任務(wù)3 { internal class Program { static CancellationTokenSource source = new CancellationTokenSource(); static void Main(string[] args) { Task.Run(() => { for (int i = 0; i < 10; i++) { Thread.Sleep(100); Console.WriteLine("oh my god"); source.Token.ThrowIfCancellationRequested(); } }, source.Token); Thread.Sleep(2000); Console.WriteLine("取消任務(wù)"); source.Cancel(); Console.ReadKey(); } } }
運(yùn)行:
這個(gè)可以取消任務(wù)是吧,那下面換一個(gè)寫法:
namespace 取消任務(wù)3 { internal class Program { static CancellationTokenSource source = new CancellationTokenSource(); static void Main(string[] args) { Task.Run(() => { Thread.Sleep(3000); Console.WriteLine("oh my god"); source.Token.ThrowIfCancellationRequested(); }, source.Token); Thread.Sleep(1000); Console.WriteLine("取消任務(wù)"); source.Cancel(); Console.ReadKey(); } } }
任務(wù)中等待三秒,我在等待一秒后取消任務(wù),看看結(jié)果:
這回就不管用了,任務(wù)明明取消了,但結(jié)果依然執(zhí)行了。
根據(jù)他們寫的例子,可以總結(jié)一點(diǎn),就是要想取消任務(wù),在任務(wù)中,必須加入 for 或者 while 循環(huán),并且在下一輪循環(huán)中,執(zhí)行到 source.Token.ThrowIfCancellationRequested() 這句才能取消任務(wù)。
換個(gè)寫法,如果非得用 for 或者 while 循環(huán)這樣的語(yǔ)法才能取消任務(wù),我在 while 循環(huán)中加入一個(gè)判斷,如果等于 true,直接跳出循環(huán),這不也是中斷了任務(wù),所以說(shuō) CancellationTokenSource 真的意義不大
namespace 取消任務(wù)4 { internal class Program { static void Main(string[] args) { bool isOut = false; var task1 = Task.Run(() => { for (int i = 0; i < 100; i++) { if (isOut) return; Console.WriteLine("執(zhí)行中" + i); Thread.Sleep(500); } }); Thread.Sleep(2000); Console.WriteLine("取消任務(wù)"); isOut= true; Console.ReadKey(); } } }
運(yùn)行:
三、Task取消任務(wù)的回調(diào)
取消任務(wù)也是可以加入回調(diào)的,如下:
namespace 取消任務(wù)2 { internal class Program { static CancellationTokenSource source = new CancellationTokenSource(); static void Main(string[] args) { var task1 = Task.Run(() => { for (int i = 0; i < 100; i++) { source.Token.ThrowIfCancellationRequested(); Console.WriteLine("執(zhí)行中" + i); Thread.Sleep(500); } }, source.Token); //在指定的毫秒數(shù)后取消task執(zhí)行 source.CancelAfter(2 * 1000); //取消任務(wù)后的回調(diào) source.Token.Register(() => { //不延遲會(huì)獲取不到正確的狀態(tài) Thread.Sleep(50); Console.WriteLine("task1狀態(tài):" + task1.Status); Console.WriteLine("IsFaulted狀態(tài):" + task1.IsFaulted);//由于未處理的異常,任務(wù)已完成。 Console.WriteLine("IsCompleted狀態(tài):" + task1.IsCompleted);//獲取一個(gè)值,該值指示任務(wù)是否已完成。 }); Console.ReadKey(); } } }
運(yùn)行:
四、Task超時(shí)處理的實(shí)現(xiàn)
在上面的介紹中可以看到,Task取消任務(wù)傳統(tǒng)的用法并不好用,必須在里面加上條件判斷,如果滿足條件就跳出 for 或者 while 循環(huán),達(dá)到方法執(zhí)行完成的目的,而并不是真的終止了任務(wù)。
那么這么需求要如何去完成呢,微軟官方也提供了一個(gè)叫 Task.WhenAny 接口,可以實(shí)現(xiàn)這個(gè)功能,下面就看看如何實(shí)現(xiàn)的。
新建一個(gè)基于 .Net6 的 Winform 項(xiàng)目,新建一個(gè)腳本 Lib.cs
namespace Utils { public static class Lib { public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout, Action<TResult>? successor = null) { //用于取消任務(wù) using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource(); //WhenAny 等待所有任務(wù)結(jié)束,這里加入了超時(shí)時(shí)間 //ConfigureAwait 配置用來(lái)等待 任務(wù)1的警報(bào),返回值可以獲取到改任務(wù) Task? completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)).ConfigureAwait(continueOnCapturedContext: false); //如果當(dāng)前任務(wù)完成了,并且匹配 if (completedTask == task) { //取消任務(wù) timeoutCancellationTokenSource.Cancel(); //得到任務(wù)返回結(jié)果 var result = await task.ConfigureAwait(continueOnCapturedContext: false); //執(zhí)行回調(diào) if(successor != null) successor(result); return true; } else //任務(wù)超時(shí) return false; } } }
界面如下,就幾個(gè)按鈕
代碼:
using Utils; namespace 異步編程 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Test1(); } private void button2_Click(object sender, EventArgs e) { Test2(); } private void Button_ClearConsole_Click(object sender, EventArgs e) { Console.Clear(); } private async void Test1() { var task1 = Task.Run(() => { Thread.Sleep(3000); return "task1"; }); bool res1 = await task1.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) => { Console.WriteLine("-----------------------task1回調(diào):" + msg); }); string isTimeout1 = res1 == true ? "沒超時(shí)" : "超時(shí)"; Console.WriteLine("任務(wù)1:" + isTimeout1); var task2 = Task.Run(() => { Thread.Sleep(3000); return "task2"; }); bool res2 = await task2.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) => { Console.WriteLine("-----------------------task2回調(diào):" + msg); }); string isTimeout2 = res2 == true ? "沒超時(shí)" : "超時(shí)"; Console.WriteLine("任務(wù)2:" + isTimeout2); } private async void Test2() { var task3 = Task.Run(() => { Thread.Sleep(3000); return "task3"; }); bool res3 = await task3.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) => { Console.WriteLine("-----------------------task3回調(diào):" + msg); }); string isTimeou3 = res3 == true ? "沒超時(shí)" : "超時(shí)"; Console.WriteLine("任務(wù)2:" + isTimeou3); } } }
按鈕1和按鈕2 方法里有三個(gè)異步方法,超時(shí)時(shí)間都是4秒,也就是說(shuō),如果方法在4秒之內(nèi)沒有返回值則為失敗。
分別點(diǎn)擊按鈕1,按鈕2
在按鈕1方法里有兩個(gè)異步方法,異步方法1執(zhí)行完成后,才能執(zhí)行異步方法2,所以異步方法2要比異步方法3更慢一些。
下面就將三個(gè)異步方法的超時(shí)時(shí)間改為1秒,看看效果:
返回超時(shí),而且任務(wù)也沒有執(zhí)行,這樣就實(shí)現(xiàn)了我們的想要的效果了。
五、Task.WhenAny 的異常
在一系列的測(cè)試中,我發(fā)現(xiàn)了 Task.WhenAny 這個(gè)接口在 .Net6 的控制臺(tái)項(xiàng)目的異常之處,執(zhí)行一次是正常的,如果在一個(gè)方法內(nèi)同時(shí)執(zhí)行多次,返回結(jié)果就開始亂了,在 Winform 項(xiàng)目中是沒有這種事的,下面開始演示。
新建一個(gè)基于 .Net6 的控制臺(tái)項(xiàng)目, 將上面的 Lib.cs 代碼復(fù)制到項(xiàng)目中來(lái)。
代碼:
using Utils; namespace 異步編程2 { internal class Program { static void Main(string[] args) { AwaitReturnValue(); Console.ReadKey(); } public static async void AwaitReturnValue() { var task1 = Task.Run(() => { Thread.Sleep(3000); return "task1"; }); for (int i = 0; i < 10; i++) { bool res1 = await task1.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), (string msg) => { Console.WriteLine("task1回調(diào):" + msg); }); string isTimeout = res1 == true ? "沒超時(shí)" : "超時(shí)"; Console.WriteLine(string.Format("結(jié)果{0}:{1}", i, isTimeout)); } } } }
運(yùn)行:
任務(wù)前兩次是正確的,后面返回的基本全是錯(cuò)誤的,原因我估計(jì)是 Task 任務(wù)內(nèi)部等待時(shí)間是3秒,調(diào)用了前兩次時(shí)間沒有超過(guò)三秒,所以返回是正確的,后面超過(guò)3秒后,全當(dāng)在超時(shí)范圍內(nèi)返回了。
六、其他的寫法
超時(shí)取消任務(wù)的寫法可以有多種,其實(shí)萬(wàn)變不離其宗,都是用 Task.WhenAny 方法實(shí)現(xiàn)的,代碼我全部放一個(gè)類里面了,有興趣的可以看看,有很多的高級(jí)語(yǔ)法,確實(shí)是值得學(xué)習(xí)的。
代碼:
namespace 異步編程1 { public static class Lib1 { public static async Task<TResult?> TimeoutAfter<TResult>(this Task<TResult> task, int timeout) { using (var cancelToken = new CancellationTokenSource()) { Task completedTask = await Task.WhenAny(task, Task.Delay(timeout, cancelToken.Token)); if (completedTask == task) { cancelToken.Cancel(); return await task; } else { // 超時(shí)處理 Console.WriteLine("超時(shí)了"); return default; } } } public static async Task<bool> OnTimeout<T>(T t, Action<T> action, int waitms) where T : Task { if (!(await Task.WhenAny(t, Task.Delay(waitms)) == t)) { action(t); return true; } else { return false; } } public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout, Action<TResult> successor) { using var timeoutCancellationTokenSource = new CancellationTokenSource(); var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)).ConfigureAwait(continueOnCapturedContext: false); if (completedTask == task) { timeoutCancellationTokenSource.Cancel(); // propagate exception rather than AggregateException, if calling task.Result. var result = await task.ConfigureAwait(continueOnCapturedContext: false); successor(result); return true; } else return false; } public static async Task<bool> BeforeTimeout(Task task, int millisecondsTimeout) { if (task.IsCompleted) return true; if (millisecondsTimeout == 0) return false; if (millisecondsTimeout == Timeout.Infinite) { await Task.WhenAll(task); return true; } var tcs = new TaskCompletionSource<object>(); using (var timer = new Timer(state => ((TaskCompletionSource<object>)state).TrySetCanceled(), tcs, millisecondsTimeout, Timeout.Infinite)) { return await Task.WhenAny(task, tcs.Task) == task; } } } }
到此這篇關(guān)于C# async/await任務(wù)超時(shí)處理的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)C# async/await任務(wù)超時(shí)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
探討Object轉(zhuǎn)為String的幾種簡(jiǎn)易形式詳解
本篇文章是對(duì)Object轉(zhuǎn)為String的幾種簡(jiǎn)易形式進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06C#處理類型和二進(jìn)制數(shù)據(jù)轉(zhuǎn)換并提高程序性能
這篇文章介紹了C#處理類型和二進(jìn)制數(shù)據(jù)轉(zhuǎn)換并提高程序性能的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04C# ComboBox的聯(lián)動(dòng)操作(三層架構(gòu))
這篇文章主要介紹了C# ComboBox的聯(lián)動(dòng)操作(三層架構(gòu)),根據(jù)下拉框的變化使得下拉框綁定對(duì)應(yīng)值,感興趣的小伙伴們可以參考一下2016-05-05c#的時(shí)間日期操作示例分享(c#獲取當(dāng)前日期)
這篇文章主要介紹了c#的時(shí)間日期操作示例,在給定時(shí)間戳返回指定的時(shí)間格式和獲取當(dāng)前時(shí)間方法,需要的朋友可以參考下2014-03-03Unity實(shí)現(xiàn)卡片循環(huán)滾動(dòng)效果的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何利用Unity實(shí)現(xiàn)卡片循環(huán)滾動(dòng)的效果,文中的實(shí)現(xiàn)步驟講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以參考一下2022-12-12C# Record構(gòu)造函數(shù)的行為更改詳解
C# 9 中的record類型是僅具有只讀屬性的輕量級(jí)、不可變數(shù)據(jù)類型(或輕量級(jí)類),下面這篇文章主要給大家介紹了關(guān)于C# Record構(gòu)造函數(shù)的行為更改的相關(guān)資料,需要的朋友可以參考下2021-08-08C#實(shí)現(xiàn)Winform版計(jì)算器
這篇文章主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)Winform版計(jì)算器,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05Unity3D利用DoTween實(shí)現(xiàn)卡牌翻轉(zhuǎn)效果
這篇文章主要為大家詳細(xì)介紹了Unity3D利用DoTween實(shí)現(xiàn)卡牌翻轉(zhuǎn)效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02