[英]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:旁注:
CancellationTaskSource
in TimeoutAfter
doesn't do anything. TimeoutAfter
中的CancellationTaskSource
不执行任何操作。TimeoutAfter
can be replaced with WaitAsync
in .NET 6 and newer.TimeoutAfter
可以替换为WaitAsync
。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.