您正在使应用程序死锁,因为您没有使用 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 timeout 构造函数重载(首选),使用替代的更好的超时方法来修复第一个死锁:
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:
访问属性的 [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 线程上执行延续队列。