Еще один аргумент за ConfigureAwait(false) в библиотеках

Как получить блокировку ASP.NET приложения одним вызовом .NET метода? Очень даже просто. Причем, данный способ справедлив также для WPF и WinForms.

Причина кроется в неудачной попытке объединения двух миров: синхронного и асинхронного. Рассмотрим проблему на примере.

Демонстрация проблемы

Создадим ASP.NET MVC приложение. Добавим в него Контроллер Home c Действием Index и соответствующим Представлением. В данный момент все работает отлично.

Теперь предположим, что необходимо обратиться к сервису. Но вот проблема - он асинхронный. Чтобы не переписывать код Действия воспользуемся методом Wait(). Он превратит вызов в синхронный:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var service = new SomeService();
        service.DoSomeJobAsync().Wait();

        return this.View();
    }
}

public class SomeService
{
    public async Task DoSomeJobAsync()
    {
        await Task.Delay(200);
        Thread.Sleep(100);
    }
}

На первый взгляд все замечательно. Но если теперь запустить приложение, то результатом будет блокировка выполнения кода. Давайте разберемся почему это происходит:

  • Вызов Wait() блокирует основной поток до завершения вызова метода DoSomeJobAsync().
  • Поскольку async/await не используются для вызова DoSomeJobAsync(), то его код начинает выполняется в основном потоке.
  • В сервисе расположен вызов асинхронного метода. Теперь вспомним, что async/await это конечный автомат, который использует очередь сообщений. Он сохранит текущий контекст (основного потока) и ему же будет отправлено сообщение о завершении Task.Delay(). Но основной поток находится в режиме ожидания из-за вызова Wait(). Итог – блокировка сразу после завершения Task.Delay().

С аналогичной ситуацией можно столкнуться в WPF и WinForms приложениях при попытке вызова из UI-потока асинхронных методов как синхронные.

Способы решения

Вариант 1: Исправляем Контроллер

Первый вариант очень простой. Если возможно, просто сделать метод Контроллера асинхронным:

public async Task<ActionResult> Index()
{
    var service = new SomeService();
    await service.DoSomeJob();

    return this.View();
}

Вариант 2: Исправляем библиотеку (сервис)

Второй способ универсальный. Как упоминалось уже в заметке “Оптимизируем async/await в библиотеках”, внутри кода библиотек нет смысла сохранять контекст синхронизации. Изменим сервис:

public async Task DoSomeJobAsync()
{
        await Task.Delay(200).ConfigureAwait(false);
        Thread.Sleep(100);
}

Теперь вызов ConfigureAwait(false) отменяет сохранение контекста синхронизации. Это означает, что выполнение кода DoSomeJobAsync() после await Task.Delay() продолжится в контексте по умолчанию*.* Соответственно, нет необходимости в очереди сообщений. Блокировки не будет и выполнение кода продолжится.

Это еще одна причина “превентивного” использования ConfigureAwait(false) в библиотеках.