天天看點

記一次.net core 異步線程設定逾時時間

前言:

刷文章看到一篇 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;
    }
           

測試截圖

記一次.net core 異步線程設定逾時時間
記一次.net core 異步線程設定逾時時間
記一次.net core 異步線程設定逾時時間
記一次.net core 異步線程設定逾時時間

搞定收工

故事在這裡就結束了嗎? 顯然沒有,這麼簡單也沒必要水一篇部落格了

記一次.net core 異步線程設定逾時時間

我們能做到在3s内響應結果,也算基本上滿足了需求,那逾時的子任務,是否會繼續執行呢?

仔細看代碼,就算逾時,也是停止的Task.Delay() 這個線程,與業務線程沒有半毛錢關系,那業務線程肯定會繼續執行

眼尖的同學已經看到最後一張圖,B任務執行了3.0076088s,按道理B任務是已經逾時了,這段話是不會輸出的,那如果我讓主線程晚點退出,那逾時的子線程是否能正常執行, //await Task.Delay(3000); 将這段代碼取消注釋,再來觀看結果

記一次.net core 異步線程設定逾時時間

有沒有一種被欺騙的感覺,寫了一個假的逾時時間,哈哈哈哈.....

記一次.net core 異步線程設定逾時時間

如果寫了一個死循環的task,那後果将不堪設想,這個時候,就需要慎重了,在使用多線程取消令牌的時候,除了需要執行Cancel()方法,還需要在子任務内自己捕獲CancellationTokenSource.Token.ThrowIfCancellationRequested()