Мне никогда не нравилась многословность кода. Длинные и подробные названия упрощают работу с бизнес-логикой, но технические детали кода хочется держать краткими, чтобы они отвлекали на себя минимум внимания. Одна из многословных конструкций .NET связана с деталями реализации асинхронности и обросла кучей мифов. Про неё спрашивают на собеседованиях, код-ревью, делают обязательной, добавляя в правила линтера. Это .ConfigureAwait(false), сопровождающий каждый await в коде. В этой статье я расскажу, зачем нужен ConfigureAwait(false) и как обойтись без него. Перед тем, как перейти к ConfigureAwait напомню, что такое асинхронный код, где у таска continuation, и что такое SynchronizationContext. Как именно компилятор трансформирует код можно посмотреть, например, на sharplab.io Выше приведён код обработчика события нажатия на кнопку. Где будет выполняться continuation этого обработчика событий после завершения асинхронного Task? Нет, не в Thread Pool. Все действия с UI должны производиться в одном потоке, на котором крутится event loop. Этот код будет работать только в том случае, если continuation, обновляющий содержимое TextBox Text вернётся на UI-поток, в котором началась обработка события. Для этого UI-фреймворки устанавливают SynchronizationContext, который возвращает continuation в очередь основного потока. Без SynchronziationContext пришлось бы явно перекладывать UI-код на UI-поток: SynchronizationContext встречается не только в UI-коде. Например, xUnit переопределяет его, для отслеживания async void методов и обработки исключений в них. В старом ASP.NET тоже был задан SynchronizationContext для доступа к HttpContext. К счастью, в ASP.NET Core его нет. Кроме SynchronizationContext также может быть переопределён TaskScheduler, примерно с теми же последствиями. Разработчик UI может ожидать выполнения метода GetTextAsync синхронно (или в коде используется библиотека с плохим, синхронным ожиданием внутри). В этом случае: В некоторых случаях deadlock может не произойти: если GetTextAsync выполнится синхронно, либо если в нём произойдёт переход в другой контекст, например на Thread Pool. Стоит отметить, что желательно избегать блокирующего ожидания, особенно на UI-потоке. Даже если deadlock не произойдёт, во время блокировки UI-потока программа будет выглядеть зависшей. Если GetTextAsync ожидается асинхронно (с использованием await), то возникнет другая проблема. Контекст синхронизации попадает в метод GetTextAsync и его continuation с методом Deserialize тоже выполнится на UI-потоке. Блокировки не будет, но UI-поток во время выполнения этого метода не сможет выполнять более полезную нагрузку. Если на UI-поток попадает много лишнего кода, который мог бы выполняться в фоне — приложение станет менее отзывчивым. Из-за этих проблем и сложности их отлова, в .NET сложилась практика писать код, который потенциально может быть вызван внутри SynchronizationContext (т.е. в коде библиотек) так, чтобы эти проблемы не возникли, каким бы этот контекст не был. А средство для этого — .ConfigureAwait(false), обеспечивающий перекладывание continuation на Thread Pool. В этом случае, continuation метода GetTextAsync будет выполняться на потоке из Thread Pool, а возврат в исходный контекст синхронизации произойдёт лишь при выходе из GetTextAsync — в результате, Btn_OnClick continuation выполнится на UI потоке, как и ожидалось изначально. Если await выполнится синхронно — переход в Thread Pool не произойдёт. Отсюда берётся рекомендация использовать .ConfigureAwait(false) вместе с каждым await. Также, .ConfigureAwait(false) лишает вызывающий код возможности управлять тем, где будет выполняться асинхронный код переопределением SynchronizationContext и TaskScheduler. Какая-то часть кода "сбежит" в стандартный Thread Pool из-за повсеместного использования .ConfigureAwait(false). .ConfigureAwait(true) задаёт поведение по-умолчанию и не несёт в себе никакого смысла. Кроме Task/Task<T> .ConfigureAwait(false) актуален для ValueTask/ValueTask<T>, IAsyncEnumerable<T> и IAsyncDisposable (и некоторых других типов). Особую боль представляет собой IAsyncDisposable. Обёртка ConfiguredAsyncDisposable — не generic, и не даёт возможности получить доступ к оригинальному объекту, в результате требуется разделять создание объекта и его использование в конструкции using. Область видимости переменной при этом выходит за границы блока using, что создаёт риск ошибки в коде. Наивно считать, что во всём используемом коде расставлены .ConfigureAwait(false). Можно изначально писать код, выполняющийся в контексте синхронизации так, чтобы ему было безразлично, как написаны await в вызываемом коде. Этого можно достичь, если запускать весь код, которому не нужен контекст синхронизации на Thread Pool, например с помощью Task.Run. Делегат, переданный в Task.Run будет выполнен без контекста синхронизации — на стандартном Thread Pool. В отсутствии контекста синхронизации ConfugureAwait не несёт смысла. Task, возвращённый методом Task.Run ожидается уже в контексте синхронизации, поэтому continuation Btn_OnClick будет выполнен на UI-потоке и значение в Text успешно изменится. Этот способ переносит сложность в вызывающий код, но имеет несколько преимуществ: Также, в Task.Run можно обернуть не только асинхронный метод, но и синхронный, например на случай, если внутри есть блокировка, приводящая к deadlock, или для выноса тяжелых вычислений в фон. Предыдущий способ будет также работать и при синхронном ожидании. Можно сделать синхронную обёртку над асинхронным методом, которая в отличие от простого .Wait()/.Result/.GetAwaiter().GetResult() будет устойчива к описанному deadlock. В идеальном мире хочется, чтобы синхронная версия метода была реализована отдельно и не задействовала Thread Pool. Часто это нереалистично из-за необходимости поддерживать две реализации сразу. Этот способ как раз для таких случаев. Этот способ предназначен для разработчиков асинхронного кода в библиотеках, которым нужно гарантировать работу кода независимо от SynchronizationContext и того, как код используется извне, но нет желания засорять код .ConfigureAwait(false). Вместо повсеместных .ConfigureAwait(false) в вызываемом коде, предлагается написать конструкцию, уводящую выполнение метода на Thread Pool один раз в начале метода. Можно ограничиться только публичными методами. При таком способе переход на Thread Pool произойдёт сразу, до первого асинхронного await. Это может снизить производительность, если обычно все await выполняются синхронно, без смены потока, или наоборот, повысить — если до первого асинхронного await выполняется вычислительный код, зря занимающий UI-поток. В реальных условиях разницу вряд ли получится заметить вовсе. Способ подсмотрен в dotnet/runtime. Также есть issue о добавлении публичного API и готовая реализация в Microsoft.VisualStudio.Threading. Ниже приведена реализация, переходящая в Thread Pool только если задан контекст синхронизации или TaskScheduler: Однако, есть случай, когда такой подход не сработает — при реализации IAsyncEnumerable<T>. В этом случае, если вызывающий метод имеет SynchronizationContext, то итератор будет получать контекст при каждом вызове .MoveNextAsync(). В итоге, уход с контекста потребуется делать заново после каждого yield return. Чтобы не писать .ConfigureAwait(false) вручную — их можно сгенерировать сразу по всей сборке или для отдельных классов и методов. Например, с помощью ConfigureAwait.Fody. На мой взгляд, предыдущее решение лучше, как более явное, но если в проекте уже используется Fody, то выбрать этот вариант вполне логично. Проблемы, возникающие при работе асинхронного кода во внешнем контексте синхронизации могут быть решены и без повсеместного иcпользования .ConfigureAwait(false), причём как со стороны вызывающего, так и со стороны вызываемого кода. Сейчас уже сложно сказать, почему работа с контекстом синхронизации в C# была спроектирована именно таким образом. Да и это лишено смысла — изменить это уже невозможно, т.к. это огромный breaking change. В .NET сложилась практика использования .ConfigureAwait(false) в коде библиотек, однако это не является обязательным: Так что на вопрос "нужен ли ConfigureAwait?" можно ответить: если вы его не используете и никто не жалуется — не нужен. А если уже используете, то всё зависит от кода, который ваш код использует.