此异步/等待代码引起的潜在死锁?

时间:2018-11-05 23:55:47

标签: c# .net multithreading async-await azureservicebus

我有以下课程:

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;
}

从我的EnqueueMessage方法中调用CreateBooking方法时,程序挂起,并且在碰到行await topicClient.SendAsync(message.ToServiceBusMessage());之后不再前进

现在,当我按以下方式更改对EnqueueMessage方法的调用时,代码可以正常工作并且我的消息成功发送到服务总线:

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

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

2 个答案:

答案 0 :(得分:10)

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

  1. 写上“割草”的便条放在冰箱上。
  2. 在注释中提到的任务完成之前,什么也不做。
  3. 做一个三明治
  4. 执行写在冰箱笔记上的任务。

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

您正在用软件有效地编码相同的工作流程,因此也就永远等待着也就不足为奇了。

那为什么要添加一个Run“修复”呢?这是另一个工作流程:

  1. 写一张便条,上面写着“割草”,雇用一名工人,然后将其交给工人
  2. 在注释中提到的任务完成之前,绝对不要做任何事情
  3. 做一个三明治

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

这就是为什么您永远不会同步等待异步操作的原因。仅有两种可能性:如果在将来进行异步操作,则您将永远等待,这显然被打破了。如果不是,那么您在进行工作时会浪费时间,这显然是浪费的。

按照设计的异步方式使用:异步

答案 1 :(得分:1)

您是正确的,这是您看到的异步死锁。最好的做法是始终等待Task返回函数,以使您的代码从上到下都是异步的。

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

Task.Run在此进行修复的原因是,它导致内部的委托在没有当前SynchronizationContext的情况下运行,这就是等待状态捕获的(当不使用.ConfigureAwait(false)时)并导致阻塞结果时陷入僵局。