我有一个方法定义为:
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)
{
}
}
它工作得很好,但是当负载很大时,我发现线程正在急剧增加并且转储向我显示以下警告:
谁能建议我处理这种情况的最佳方法,以便我看不到任何类型的死锁?
请您参考如下方法:
您正在使应用程序死锁,因为您没有使用 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;
}
来自 Microsoft Docs :
Accessing the property's [
Task.Result] get accessor blocks the calling thread until the asynchronous operation is complete; it is equivalent to calling the Wait method.
死锁的原因和前面解释的一样:一个阻塞的
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 上继续执行延续队列永远不会是父线程的线程。
