此异步/等待代码可能导致死锁



我有以下类:

public abstract class ServiceBusQueueService : IServiceBusQueueService
{
private readonly string _sbConnect;
protected ServiceBusQueueService(string sbConnect)
{
_sbConnect = sbConnect;
}
public async Task EnqueueMessage(IntegrationEvent message)
{
var topicClient = new TopicClient(_sbConnect, message.Topic, RetryPolicy.Default);
await topicClient.SendAsync(message.ToServiceBusMessage());
}
}

它是这样使用的:

public ulong CreateBooking()
{
// Other code omitted for brevity 
ulong bookingId = 12345; // Pretend this id is generated sequentially on each call
_bookingServiceBusQueueService.EnqueueMessage(new BookingCreatedIntegrationEvent
{
BookingId = bookingId
}).GetAwaiter().GetResult();
return bookingId;
}

当从我的CreateBooking方法调用EnqueueMessage方法时,程序挂起,并且在到达await topicClient.SendAsync(message.ToServiceBusMessage());行后不再继续

现在代码工作了,当我将调用更改为EnqueueMessage方法时,我的消息成功地推送到服务总线,如下所示:

Task.Run(() => _bookingServiceBusQueueService.EnqueueMessage(new BookingCreatedIntegrationEvent
{
BookingId = bookingId
})).Wait();

我不太熟悉使用async/await,经过一点研究,它听起来像是导致了死锁,这是正确的吗?为什么将方法调用更改为Task.Run(() => SomeAsyncMethod()).Wait();会导致它停止挂起并按预期工作?

假设我给了您以下工作流程:

  1. 写一张纸条,上面写着"修剪草坪",然后放在冰箱上
  2. 在注释中提到的任务完成之前,不要做任何事情
  3. 做一个三明治
  4. 完成写在冰箱笔记上的任务

如果您遵循该工作流,您将进入步骤(2(,然后永远不做任何事情,因为您正在等待步骤(1(的任务完成,直到步骤(4(才开始。

你在软件中有效地编码了相同的工作流程,所以你永远在等待也就不足为奇了。

那么,为什么添加Run可以"修复"它呢?这是另一个工作流程:

  1. 写一张纸条,上面写着"修剪草坪",雇佣一名工人,并将纸条交给工人
  2. 在笔记中提到的任务完成之前,什么都不做
  3. 做一个三明治

现在你不会永远等待。你等着工人来修剪你的草坪,这只是效率低下和浪费。你可以在等待的时候做其他工作,比如做三明治。或者你可以自己修剪草坪,而不用承担雇佣工人的费用。

这就是为什么您永远不会同步等待异步操作的原因。只有两种可能性:如果在未来进行异步操作,那么您将永远等待,这显然是坏的。如果你没有,那么你就是在浪费时间睡觉,而你本可以在工作,这显然是浪费。

按照设计使用异步:异步

你是对的,这是一个异步死锁。最好的做法是始终等待Task返回函数,这样您的代码从上到下是异步的。

您还可以通过在EnqueueMessage内部的等待之后使用.ConfigureAwait(false)来避免死锁。

Task.Run在这里修复它的原因是,它导致内部的委托在没有当前SynchronizationContext的情况下运行,正是这一点被wait捕获(当没有使用.ConfigureAwait(false)时(,并在阻塞结果时导致死锁。

最新更新