我有以下类:
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();
会导致它停止挂起并按预期工作?
假设我给了您以下工作流程:
- 写一张纸条,上面写着"修剪草坪",然后放在冰箱上
- 在注释中提到的任务完成之前,不要做任何事情
- 做一个三明治
- 完成写在冰箱笔记上的任务
如果您遵循该工作流,您将进入步骤(2(,然后永远不做任何事情,因为您正在等待步骤(1(的任务完成,直到步骤(4(才开始。
你在软件中有效地编码了相同的工作流程,所以你永远在等待也就不足为奇了。
那么,为什么添加Run
可以"修复"它呢?这是另一个工作流程:
- 写一张纸条,上面写着"修剪草坪",雇佣一名工人,并将纸条交给工人
- 在笔记中提到的任务完成之前,什么都不做
- 做一个三明治
现在你不会永远等待。你等着工人来修剪你的草坪,这只是效率低下和浪费。你可以在等待的时候做其他工作,比如做三明治。或者你可以自己修剪草坪,而不用承担雇佣工人的费用。
这就是为什么您永远不会同步等待异步操作的原因。只有两种可能性:如果您在未来进行异步操作,那么您将永远等待,这显然是坏的。如果你没有,那么你就是在浪费时间睡觉,而你本可以在工作,这显然是浪费。
按照设计使用异步:异步。
你是对的,这是一个异步死锁。最好的做法是始终等待Task返回函数,这样您的代码从上到下是异步的。
您还可以通过在EnqueueMessage
内部的等待之后使用.ConfigureAwait(false)
来避免死锁。
Task.Run
在这里修复它的原因是,它导致内部的委托在没有当前SynchronizationContext的情况下运行,正是这一点被wait捕获(当没有使用.ConfigureAwait(false)
时(,并在阻塞结果时导致死锁。