繁体   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-2023 STACKOOM.COM