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