前言:
刷帖子看到一篇 Go 記錄一次groutine通訊與context控制 看了一下需求背景,挺有意思的,琢磨了下.net core下的實現
需求背景:
專案中需要定期執行任務A來做一些輔助的工作,A的執行需要在超時時間內完成,如果本次執行超時了,那就不對本次的執行結果進行處理(即放棄這次執行)。同時A又依賴B,C兩個子任務的執行結果。B, C之間相互獨立,可以並行的執行。但無論B,C哪一個執行失敗或超時都會導致本次任務執行失敗。
需求提煉:
- A任務必須在指定時間內完成,否則任務失敗
- A任務依賴B,C任務,B,C可以並行,任何一個失敗,則A任務失敗
- A任務在超時時間內,是否需求記錄子任務執行詳情(根據業務需求來定)
.net裡設定超時的 Task
public static class TaskHelper
{
// 有返回值
public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
{
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
if (completedTask == task)
{
timeoutCancellationTokenSource.Cancel();
return await task; // Very important in order to propagate exceptions
}
else
{
throw new TimeoutException("The operation has timed out.");
}
}
}
// 無返回值
public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
using (var timeoutCancellationTokenSource = new CancellationTokenSource())
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
if (completedTask == task)
{
timeoutCancellationTokenSource.Cancel();
await task; // Very important in order to propagate exceptions
}
else
{
throw new TimeoutException("The operation has timed out.");
}
}
}
}
這裡參考資料,寫了個擴充方法,主要用到CancellationTokenSource 與 Task.WhenAny
可以參考 C#中CancellationToken和CancellationTokenSource用法
可以參考 Task.WhenAny 方法
這裡需要特別注意,在非同步操作裡,如果非同步已經執行了,再執行取消時無效的,這是就需要我們自己在非同步委託中檢測了
知道了這兩個函式的作用,這段程式碼就很好理解了,通過Task.WhenAny返回最先完成的任務,如果是業務任務先完成,則呼叫timeoutCancellationTokenSource.Cancel()終止超時任務,等待業務任務結果,反之則直接丟擲timeout異常
測試程式碼
[TestMethod]
public async Task TestMethod1()
{
//A 任務必須在指定時間內完成,否則任務失敗
//A 任務依賴B,C任務,B,C可以並行,任何一個失敗,則A任務失敗
//A任務
try
{
//有效時間3s
var timeOut = TimeSpan.FromSeconds(3);
await Task.Run(async () =>
{
List<Task<(string, bool)>> tasks = new List<Task<(string, bool)>>();
//B任務
tasks.Add(Task.Run(async () =>
{
return ("B", await TestTask("B"));
}).TimeoutAfter(timeOut));
//C任務
tasks.Add(Task.Run(async () =>
{
return ("C", await TestTask("C"));
}).TimeoutAfter(timeOut));
var res = await Task.WhenAll(tasks);
//兩個任務,任何一個失敗,則A任務失敗
foreach (var item in res)
{
Console.WriteLine(item);
}
}).TimeoutAfter(timeOut);
}
catch (Exception ex)
{
Console.WriteLine("A任務執行超時了");
}
//await Task.Delay(3000);
}
public async Task<bool> TestTask(string name)
{
var startTime = DateTime.Now;
Console.WriteLine($"{startTime}---->{name}任務開始執行");
//隨機堵塞1-5s
var t = new Random().Next(1, 5);
await Task.Delay(t * 1000);
var endTime = DateTime.Now; ;
var time = (endTime - startTime).TotalSeconds;
//隨機數,模擬業務是否成功
var res = new Random().Next(1, 10);
Console.WriteLine($"{endTime}---->{name}任務執行完畢,耗時{time} s");
return res <= 7;
}
測試截圖
搞定收工
故事在這裡就結束了嗎? 顯然沒有,這麼簡單也沒必要水一篇部落格了
我們能做到在3s內響應結果,也算基本上滿足了需求,那超時的子任務,是否會繼續執行呢?
仔細看程式碼,就算超時,也是停止的Task.Delay() 這個執行緒,與業務執行緒沒有半毛錢關係,那業務執行緒肯定會繼續執行
眼尖的同學已經看到最後一張圖,B任務執行了3.0076088s,按道理B任務是已經超時了,這段話是不會輸出的,那如果我讓主執行緒晚點退出,那超時的子執行緒是否能正常執行, //await Task.Delay(3000); 將這段程式碼取消註釋,再來觀看結果
有沒有一種被欺騙的感覺,寫了一個假的超時時間,哈哈哈哈.....
這裡需要特別注意,在非同步操作裡,如果非同步已經執行了,再執行取消時無效的,這是就需要我們自己在非同步委託中檢測了
如果寫了一個死迴圈的task,那後果將不堪設想,這個時候,就需要慎重了,在使用多執行緒取消令牌的時候,除了需要執行Cancel()方法,還需要在子任務內自己捕獲CancellationTokenSource.Token.ThrowIfCancellationRequested()