Источник: habr
Дата: 2019-09-08
В своей практике я часто встречаю в различном окружении код вроде того, что приведен ниже:
[1] var x = FooWithResultAsync(/*...*/).Result;
//или
[2] FooAsync(/*...*/).Wait();
//или
[3] FooAsync(/*...*/).GetAwaiter().GetResult();
//или
[4] FooAsync(/*...*/).ConfigureAwait(false).GetAwaiter().GetResult();
//или
[5] await FooAsync(/*...*/).ConfigureAwait(false)
//или просто
[6] await FooAsync(/*...*/)
Из общения с авторами таких строк, стало ясно, что все они делятся на три группы:
Result/Wait/GetResult
. Примеры (1-3) и, иногда, (6), типичны для программистов из этой группы;Возможен ли риск, и на сколько он велик, при использовании кода, как в приведенных выше примерах, зависит, как я отмечал ранее, от окружения.
Примеры (1-6) делиться на две группы. Первая группа — код с блокировкой вызывающего потока. К этой группе относятся (1-4). Блокировка потока, чаще всего, плохая идея. Почему? Для простоты будем считать, что все потоки выделяются из некоторого пула потоков. Если в программе присутствует блокировка, то это может привести к выборке всех потоков из пула. В лучшем случае, это замедлит работу программы и приведет к неэффективному использованию ресурсов. В худшем же случае, это может привести к взаимоблокировке(deadlock), когда для завершения некоторой задачи, нужен будет дополнительный поток, но пул его не сможет выделить. Таким образом, когда разработчик пишет код вроде (1-4), он должен задуматься, на сколько вероятна описанная выше ситуация.
Но все становится гораздо хуже, когда мы работаем в окружении, в котором существует контекст синхронизации, отличный от стандартного. При наличии особого контекста синхронизации блокировка вызывающего потока повышает вероятность возникновения взаимоблокировки многократно. Так, код из примеров (1-3), если он выполняется в UI-потоке WinForms, практически гарантированно создает deadlock. Я пишу “практически”, т.к. есть вариант, когда это не так, но об этом чуть позже. Добавление ConfigureAwait(false)
, как в (4), не даст 100% гарантии защиты от deadlock’a. Ниже приведен пример, подтверждающий это:
[7] //Некоторый метод библиотечного / стороннего класса. async Task FooAsync() { // Delay взять для простоты. Может быть любой асинхронный вызов. await Task.Delay(5000); //Остальную часть кода метода объединим в метод RestPartOfMethodCode(); } //Код в "конечной" точке использования, в данном случае, это WinForms приложение. private void button1_Click(object sender, EventArgs e) { FooAsync() .ConfigureAwait(false) .GetAwaiter() .GetResult(); button1.Text = "new text"; }
В статье “Parallel Computing — It’s All About the SynchronizationContext” дается информация о различных контекстах синхронизации.
Для того, чтобы понять причину возникновения взаимоблокировки, нужно проанализировать код конечного автомата, в который преобразуется вызов async метода, и, далее, код классов MS. В статье “Async Await and the Generated StateMachine” приводится пример такого конечного автомата. Не буду приводить полный исходный код, генерируемого для примера (7), автомата, покажу лишь важные для дальнейшего разбора строки:
//Внутри метода MoveNext. //... // переменная taskAwaiter определена выше по коду. taskAwaiter = Task.Delay(5000).GetAwaiter(); if(tasAwaiter.IsCompleted != true) { _awaiter = taskAwaiter; _nextState = ...; _builder.AwaitUnsafeOnCompleted<TaskAwaiter, ThisStateMachine>(ref taskAwaiter, ref this); return; }
Ветка if
выполняется, если асинхронный вызов (Delay
) еще не был завершен и, следовательно, текущий поток можно освободить. Обратите внимание на то, что в AwaitUnsafeOnCompleted
передается taskAwaiter полученный от внутреннего (относительно FooAsync
) асинхронного вызова (Delay
).
Если погрузиться в дебри исходников MS, которые скрываются за вызовом AwaitUnsafeOnCompleted
, то, в конечном итоге, мы придем к классу SynchronizationContextAwaitTaskContinuation, и его базовому классу AwaitTaskContinuation, где и находятся ответ на поставленный вопрос.
Код этих, и связанных с ними, классов довольно запутан, поэтому, для облегчения восприятия, я позволю себе написать сильно упрощенный “аналог” того, во что превращается пример (7), но без конечного автомата, и в терминах TPL: