简体   繁体   English

当在循环中使用大量(ish)数量时,非异步方法的超时包装器表现不一致

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

I am using a third-party library that is non-async but can also either take longer than desired or occasionally completely block indefinitely (until cleared externally).我正在使用非异步的第三方库,但也可能需要比预期更长的时间或偶尔会无限期地完全阻塞(直到外部清除)。

Represented for testing with this:代表用这个进行测试:

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

class SlowWorkerResult 
{
    
}

To handle these timeouts I wrap the call in a Task.Run and apply an extension method I wrote to it:为了处理这些超时,我将调用包装在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;
    }
}

This works reliably whenever it is run individually, ie无论何时单独运行,它都能可靠地工作,即

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}");
    }
}

I am aware that this essentially abandons a thread .我知道这实际上放弃了一个线程 Barring support from the third-party library that isn't coming any time soon (if ever), I have no other way to protect against a deadlock.除非来自第三方库的支持不会很快出现(如果有的话),我没有其他方法来防止死锁。

However, when I run a BigWorker in a parallel loop, I get unexpected results (namely that some sessions timeout when I would otherwise expect them to complete).但是,当我在并行循环中运行BigWorker时,我得到了意想不到的结果(即某些会话超时,而我原本希望它们完成)。 For example, if I set totalWorkers to 10, I get an even split of success/failure and the process takes about 3 seconds as expected.例如,如果我将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}");
}

Setting totalWorkers to a larger number, say 100, generates an essentially random number of success/fails with the total time taking much longer.totalWorkers设置为更大的数字,比如 100,会生成基本上随机的成功/失败次数,并且总时间会花费更长的时间。

I suspect this is due to task scheduling and threadpools, however I can't figure out what I would need to do to remedy it.我怀疑这是由于任务调度和线程池引起的,但是我不知道我需要做什么来补救它。 I suspect a custom task scheduler that would somehow make sure my DoWork wrapped task and my Task.Delay are executed at the same time.我怀疑自定义任务调度程序会以某种方式确保我的DoWork包装任务和我的Task.Delay同时执行。 Right now it appears that the Task.Delay 's are occasionally being started/completed before their corresponding DoWork wrapped task.现在看来, Task.Delay偶尔会在相应的DoWork包装任务之前启动/完成。

I suspect this is due to task scheduling and threadpools我怀疑这是由于任务调度和线程池

Yes;是的; specifically, the thread pool has a limited injection rate for new threads.具体来说,线程池对新线程的注入率是有限制的。 If you need to pile on a bunch of synchronous work, then you should increase this threshold , which will cause the thread pool to quickly inject up to that threshold and switch to the limited injection rate past that threshold.如果你需要堆积一堆同步工作,那么你应该增加这个阈值,这将导致线程池快速注入到那个阈值并切换到超过那个阈值的有限注入率。

Alternatively, you could do the timeout from within the Task.Run , but that's pretty complex.或者,您可以从Task.Run中执行Task.Run ,但这非常复杂。

Side notes:旁注:

  • The CancellationTaskSource in TimeoutAfter doesn't do anything. TimeoutAfter中的CancellationTaskSource不执行任何操作。
  • TimeoutAfter can be replaced with WaitAsync in .NET 6 and newer.在 .NET 6 及更新版本中, TimeoutAfter可以替换为WaitAsync
  • The Parallel.For isn't doing anything useful; Parallel.For没有做任何有用的事情; it's just parallelizing the adding of tasks to the concurrent collection (and the tiny amount of code at the beginning of BigWorker ).它只是并行地任务添加到并发集合(以及BigWorker开头的少量代码)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM