我有一个方法定义为:
public Task<ReturnsMessage> Function() {
var task = Task.Run(() =>
{
var result = SyncMethod();
return new ReturnMessage(result);
});
if (task.Wait(delay)) {
return task;
}
var tcs = new TaskCompletionSource<ReturnMessage>();
tcs.SetCanceled();
return tcs.Task;
}
现在基于maxAttempts值在循环中调用它:
(方法名RetryableInvoke)
for (var i = 0; i < maxAttempts; i++)
{
try
{
return Function().Result;
}
catch (Exception e)
{
}
}
它工作得很好,但是当负载很大时,我发现线程急剧增加,转储向我显示此警告:
答案 0 :(得分:5)
您由于未使用async
/ await
或ConfigureAwait(false
)而陷入僵局,而是选择使用Task.Wait
和Task.Result
。
您首先应该知道Task.Run
捕获了对其执行的线程的SynchronizationContext
。然后Task.Run
在新的ThreadPool
线程上运行。完成后,它将返回到父线程以继续执行其余代码。返回时,它将返回到捕获的SyncronizationContext
。您正在使用Task.Wait
和Task.Result
破坏了这一概念。两个Task
成员将同步调用Task
,这意味着父线程将阻塞自身,直到子线程完成。子线程完成,但是Task
无法返回到捕获的SynchronizationContext
来执行其余代码(Task.Wait
之后的代码),因为父线程仍在通过等待自身阻塞自身要完成的任务。
由于您在一个地方使用了Task.Wait
而在另一地方使用了Task.Result
,因此造成了两种潜在的死锁情况:
让我们逐步介绍Function()
代码:
public Task<ReturnsMessage> Function() {
1)创建一个任务并启动它:
var task = Task.Run(
() =>
{
var result = SyncMethod();
return new ReturnMessage(result);
});
重要的事情确实在这里发生:
Task.Run
捕获当前的SynchronizationContext
,并在父线程继续执行的同时在后台开始执行。 (如果在这里使用了await
,则await
之后的其余代码将排队进入继续队列中,以供以后执行。其目的是当前线程可以返回(保留当前线程)。上下文),这样它就不需要等待和阻塞。剩下的代码将在子线程运行完成后由Task
执行,因为子线程在此之前已经在继续队列中排队。
2)task.Wait()
等待直到后台线程完成。等待意味着阻止线程继续执行。呼叫堆栈已驻留。这等于后台线程的同步,因为不再有并行执行,因为父线程不会继续执行而是阻塞:
// Dangerous implementation of a timeout for the executing `task` object
if (task.Wait(delay)) {
return task;
}
重要的事情确实在这里发生:
task.Wait()
阻止当前线程(SynchronizationContext
)等待子线程完成。子任务完成,Task
尝试从捕获的SynchronizationContext
中的连续队列中执行先前排队的剩余代码。但是,此上下文被等待子任务完成的线程阻止。潜在的僵局情况之一。
以下其余代码将无法访问:
var tcs = new TaskCompletionSource<ReturnMessage>();
tcs.SetCanceled();
return tcs.Task;
引入了 async
和await
来摆脱阻塞等待。 await
允许父线程返回并继续。 await
之后的其余代码将在捕获的SynchronizationContext
中继续执行。
这是第一个死锁的解决方案,它也使用使用Task.WhenAny
的适当任务超时解决方案(不推荐使用):
public async Task<ReturnsMessage> FunctionAsync()
{
using (var cancellationTokenSource = new CancellationTokenSource())
{
try
{
var task = Task.Run(
() =>
{
// Check if the task needs to be cancelled
// because the timeout task ran to completion first
cancellationToken.ThrowIfCancellationRequested();
var result = SyncMethod();
return result;
}, cancellationTokenSource.Token);
int delay = 500;
Task timoutTask = Task.Delay(delay, cancellationTokenSource.Token);
Task firstCompletedTask = await Task.WhenAny(task, timoutTask);
if (firstCompletedTask == task)
{
// The 'task' has won the race, therefore
// cancel the 'timeoutTask'
cancellationTokenSource.Cancel();
return await task;
}
}
catch (OperationCanceledException)
{}
// The 'timeoutTask' has won the race, therefore
// cancel the 'task' instance
cancellationTokenSource.Cancel();
var tcs = new TaskCompletionSource<string>();
tcs.SetCanceled();
return await tcs.Task;
}
}
或者使用CancellationTokenSouce
超时构造函数重载(首选),使用其他更好的超时方法修复第一个死锁:
public async Task<ReturnsMessage> FunctionAsync()
{
var timeout = 50;
using (var timeoutCancellationTokenSource = new CancellationTokenSource(timeout))
{
try
{
return await Task.Run(
() =>
{
// Check if the timeout elapsed
timeoutCancellationTokenSource.Token.ThrowIfCancellationRequested();
var result = SyncMethod();
return result;
}, timeoutCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
var tcs = new TaskCompletionSource<string>();
tcs.SetCanceled();
return await tcs.Task;
}
}
}
第二个潜在的死锁代码是Function()
的消耗量:
for (var i = 0; i < maxAttempts; i++)
{
return Function().Result;
}
访问属性的[
Task.Result
] get访问器将阻塞调用线程,直到异步操作完成为止;否则,此操作无效。 等同于调用Wait方法。
死锁的原因与前面说明的相同:阻塞的SynchronizationContext
阻止执行计划的继续。
要解决第二个死锁,我们可以使用 async / await (首选)或ConfigreAwait(false)
:
for (var i = 0; i < maxAttempts; i++)
{
return await FunctionAsync();
}
或ConfigreAwait(false)
。这种方法可用于强制异步方法的同步执行:
for (var i = 0; i < maxAttempts; i++)
{
return FunctionAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
ConfigreAwait(false)
指示Task
忽略捕获的SynchronizationContext
,并在永远不会成为父线程的另一个ThreadPool
线程上继续执行继续队列。 / p>
答案 1 :(得分:0)
您正在使用Task.Run启动任务,然后如果它们超时则返回取消,但是您永远不会停止任务。它们只是继续在后台运行。
您的代码应为异步/等待状态,并使用CancellationSource并处理SyncMethod()中的取消令牌。但是,如果不能,并且据我所知,您想要异步运行一个方法并过一会儿就将其强行杀死,则可能应该使用线程并中止它们。
警告:中止线程是不安全的,除非您知道自己在做什么,并且在以后的版本中甚至可能从.NET中删除它。
我实际上已经研究了一段时间:https://siderite.blogspot.com/2019/06/how-to-timeout-task-and-make-sure-it.html