Потенциальная тупиковая ситуация, вызванная этим асинхронным / ожидающим кодом? - PullRequest
0 голосов
/ 06 ноября 2018

У меня есть следующий класс:

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 голосов
/ 06 ноября 2018

Предположим, я дал вам следующий рабочий процесс:

  1. написать записку с надписью "косить газон" и положить ее в холодильник.
  2. ничего не делайте, пока задача, упомянутая в примечании, не будет выполнена.
  3. Сделать бутерброд
  4. Выполняйте задания, написанные на заметках в холодильнике.

Если вы следовали этому рабочему процессу, вы переходите к шагу (2), а затем ничего не делаете навсегда, потому что вы ожидаете завершения задачи шага (1), которая не запускается до шага (4).

Вы эффективно кодируете тот же рабочий процесс в программном обеспечении, поэтому неудивительно, что вы ждете вечно.

Так почему же добавление Run "исправляет" это? Вот еще один рабочий процесс:

  1. написать записку с надписью «косить газон», нанять работника и дать эту записку работнику
  2. абсолютно ничего не делать, пока задача, упомянутая в примечании, не будет выполнена
  3. Сделать бутерброд

Теперь ты не ждешь вечно. Вы ждете, когда рабочий косит вашу лужайку, которая просто неэффективна и расточительна . Пока вы ждете, вы можете выполнять другую работу, например, готовить бутерброд. Или вы можете косить газон самостоятельно и не брать на себя оплату найма рабочего.

Вот почему вы никогда, никогда не будете синхронно ждать асинхронной операции. Есть только две возможности: если вы выполняете асинхронную операцию в будущем, вы ждете вечно, что явно не работает. Если нет, то вы теряете время на сон, когда можете выполнять работу, что явно расточительно.

Используйте асинхронность так, как она была разработана: асинхронно .

0 голосов
/ 06 ноября 2018

Вы правы, это асинхронный тупик, который вы видите. Лучшее, что можно сделать, - это всегда ждать возвращения функций Task, чтобы ваш код был асинхронным сверху вниз.

Вы также можете избежать здесь тупика, используя .ConfigureAwait(false) после ожидания внутри EnqueueMessage.

Причина, по которой Task.Run исправляет это здесь, состоит в том, что это приводит к тому, что делегат внутри запускается без текущего SynchronizationContext, это то, что захватывается await (когда не используется .ConfigureAwait(false)) и приводит к тупику, когда Вы блокируете результат.

...