簡體   English   中英

當在循環中使用大量(ish)數量時,非異步方法的超時包裝器表現不一致

[英]Timeout wrapper for non-async method behaves inconsistently when large(ish) quantities are used in a loop

我正在使用非異步的第三方庫,但也可能需要比預期更長的時間或偶爾會無限期地完全阻塞(直到外部清除)。

代表用這個進行測試:

SlowWorkerResult SlowWorker(int i)
{
    var delay = i % 2 == 0 ? TimeSpan.FromSeconds(2) : TimeSpan.FromSeconds(4);
    Thread.Sleep(delay);
    return new SlowWorkerResult();
}

class SlowWorkerResult 
{
    
}

為了處理這些超時,我將調用包裝在Task.Run中並應用我寫給它的擴展方法:

static class Extensions
{
    public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
    {
        var cts = new CancellationTokenSource();
        var delayTask = Task.Delay(timeout);
        var result = await Task.WhenAny(task, delayTask);
        if (result == delayTask)
        {
            throw new TimeoutException();
        }
        cts.Cancel();
        return await task;
    }
}

無論何時單獨運行,它都能可靠地工作,即

async Task<(bool, int)> BigWorker(int i)
{
    try
    {
        Console.WriteLine($"BigWorker Started - {i}");
        
        //do some async work
        await Task.CompletedTask;

        //do some non-async work using the timeout extension
        var slowWorkerResult = await Task.Run(() => SlowWorker(i)).TimeoutAfter(TimeSpan.FromSeconds(3));

        //do some more async work
        await Task.CompletedTask;
        
        return (true, i);
    }
    catch (Exception ex)
    {
        return (false, i);
    }
    finally
    {
        Console.WriteLine($"BigWorker Finished - {i}");
    }
}

我知道這實際上放棄了一個線程 除非來自第三方庫的支持不會很快出現(如果有的話),我沒有其他方法來防止死鎖。

但是,當我在並行循環中運行BigWorker時,我得到了意想不到的結果(即某些會話超時,而我原本希望它們完成)。 例如,如果我將totalWorkers設置為 10,我會得到平均分配的成功/失敗,並且該過程按預期大約需要 3 秒。

async Task Main()
{
    var sw = new Stopwatch();
    sw.Start();
    const int totalWorkers = 10;
    
    var tasks = new ConcurrentBag<Task<(bool, int)>>();
    Parallel.For(0, totalWorkers, i => tasks.Add(BigWorker(i)));
            
    var results = await Task.WhenAll(tasks);
    sw.Stop();
    
    var success = results.Count(r => r.Item1);
    var fails = results.Count(r => !r.Item1);
    var elapsed = sw.Elapsed.ToString(@"ss\.ffff");

    Console.WriteLine($"Successes: {success}\nFails: {fails}\nElapsed: {elapsed}");
}

totalWorkers設置為更大的數字,比如 100,會生成基本上隨機的成功/失敗次數,並且總時間會花費更長的時間。

我懷疑這是由於任務調度和線程池引起的,但是我不知道我需要做什么來補救它。 我懷疑自定義任務調度程序會以某種方式確保我的DoWork包裝任務和我的Task.Delay同時執行。 現在看來, Task.Delay偶爾會在相應的DoWork包裝任務之前啟動/完成。

我懷疑這是由於任務調度和線程池

是的; 具體來說,線程池對新線程的注入率是有限制的。 如果你需要堆積一堆同步工作,那么你應該增加這個閾值,這將導致線程池快速注入到那個閾值並切換到超過那個閾值的有限注入率。

或者,您可以從Task.Run中執行Task.Run ,但這非常復雜。

旁注:

  • TimeoutAfter中的CancellationTaskSource不執行任何操作。
  • 在 .NET 6 及更新版本中, TimeoutAfter可以替換為WaitAsync
  • Parallel.For沒有做任何有用的事情; 它只是並行地任務添加到並發集合(以及BigWorker開頭的少量代碼)。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM