Ответ 1
Насколько я знаю, когда мы используем
ConfigureAwait(false)
, оставшийся метод async будет запущен в пуле потоков.
Закрыть, но есть важная оговорка, которую вы пропускаете. Когда вы возобновите работу после выполнения задачи с помощью ConfigureAwait(false)
, вы вернетесь на произвольный поток. Обратите внимание на слова "когда вы возобновляете".
Позвольте мне кое-что показать:
public async Task<string> GetValueAsync()
{
return "Cached Value";
}
public async Task Example1()
{
await this.GetValueAsync().ConfigureAwait(false);
}
Рассмотрим await
в Example1
. Хотя вы ожидаете метод async
, этот метод фактически не выполняет асинхронную работу. Если метод async
не имеет значения await
, он выполняется синхронно, и awaiter никогда не возобновляется, потому что он никогда не приостанавливается в первую очередь. Как показывает этот пример, вызовы ConfigureAwait(false)
могут быть излишними: они могут вообще не иметь никакого эффекта. В этом примере любой контекст, который вы использовали при вводе Example1
, - это контекст, который вы включите после await
.
Не совсем то, что вы ожидали, верно? И все же это не совсем необычно. Многие методы async
могут содержать быстрые пути, которые не требуют приостановки вызова вызывающего абонента. Доступность кэшированного ресурса - хороший пример (спасибо, @jakub-dąbek!), Но есть много других причин, по которым метод async
может заложить раньше. Мы часто проверяем различные условия в начале метода, чтобы увидеть, можем ли мы избежать ненужной работы, а методы async
не отличаются.
Посмотрим на другой пример, на этот раз из приложения WPF:
async Task DoSomethingBenignAsync()
{
await Task.Yield();
}
Task DoSomethingUnexpectedAsync()
{
var tcs = new TaskCompletionSource<string>();
Dispatcher.BeginInvoke(Action(() => tcs.SetResult("Done!")));
return tcs.Task;
}
async Task Example2()
{
await DoSomethingBenignAsync().ConfigureAwait(false);
await DoSomethingUnexpectedAsync();
}
Взгляните на Example2
. Первый метод we await
всегда выполняется асинхронно. К тому времени, когда мы нажмем второй await
, мы знаем, что мы работаем в потоке пула потоков, так что нет необходимости ConfigureAwait(false)
во втором вызове, правильно? Неправильно. Несмотря на наличие async
в имени и возврат Task
, наш второй метод не был написан с использованием async
и await
. Вместо этого он выполняет собственное планирование и использует TaskCompletionSource
для передачи результата. Когда вы вернетесь из своего await
, вы можете [1] закончить работу над любым потоком, предоставившим результат, который в этом случае является потоком диспетчера WPF. Упс.
Ключевым выводом здесь является то, что вы часто не знаете точно, что делает "ожидаемый" метод. С помощью или без CongifureAwait
вы можете запустить что-то неожиданное. Это может произойти на любом уровне стека вызовов async
, поэтому самый верный способ избежать непреднамеренного владения однопоточным контекстом - использовать ConfigureAwait(false)
с каждым await
, то есть во всем транзитивном закрытии.
Конечно, могут быть моменты, когда вы хотите возобновить свой текущий контекст и это прекрасно. Это якобы, почему это поведение по умолчанию. Но если вам это действительно не нужно, я рекомендую использовать ConfigureAwait(false)
по умолчанию. Это особенно верно для библиотечного кода. Библиотечный код можно вызвать из любого места, поэтому он лучше всего придерживается принципа наименьшего удивления. Это означает, что вы не блокируете другие потоки из контекста вашего звонящего, когда вам это не нужно. Даже если вы используете ConfigureAwait(false)
всюду в вашем коде библиотеки, у вашего вызывающего абонента все еще будет возможность возобновиться в исходном контексте, если это то, что они хотят.
[1] Это может варьироваться в зависимости от версии фреймворка и компилятора.