Как получить блокировку 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) в библиотеках.