Источник: 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(/*...*/)

Из общения с авторами таких строк, стало ясно, что все они делятся на три группы:

Возможен ли риск, и на сколько он велик, при использовании кода, как в приведенных выше примерах, зависит, как я отмечал ранее, от окружения.

Риски и их причины

Примеры (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: