Что делает ConfigurationAwait(false)?
Метод ConfigureAwait не является особенным: он не распознается каким-либо особым образом ни компилятором, ни средой выполнения. Это просто метод, который возвращает структуру (ConfiguredTaskAwaitable), которая обертывает исходную задачу, для которой она была вызвана, а также указанное логическое значение. Помните, что его await можно использовать с любым типом, который демонстрирует правильный шаблон. Возвращение другого типа означает, что когда компилятор обращается к GetAwaiter методу экземпляров (части шаблона), он делает это вне типа, возвращенного из задачи ConfigureAwait, а не напрямую из задачи, и это обеспечивает перехватчик для изменения поведения как await ведет себя этот пользовательский ожидающий.
В частности, ожидание возвращаемого типа ConfigureAwait(continueOnCapturedContext: false) вместо Task непосредственного ожидания влияет на показанную ранее логику захвата целевого контекста/планировщика. Это фактически делает ранее показанную логику более похожей на следующую:
Другими словами, указывая false, даже если существует текущий контекст или планировщик для обратного вызова, он делает вид, будто его нет.
Зачем мне использовать ConfigurationAwait(false)?
ConfigureAwait(continueOnCapturedContext: false) используется, чтобы избежать принудительного вызова обратного вызова в исходном контексте или планировщике. Это имеет несколько преимуществ:
Улучшение производительности. Постановка обратного вызова в очередь, а не просто его вызов, требует затрат, поскольку это требует дополнительной работы (и, как правило, дополнительного выделения ресурсов), но также и потому, что это означает, что некоторые оптимизации, которые мы в противном случае хотели бы использовать во время выполнения, не могут быть использованы ( мы можем провести дополнительную оптимизацию, когда точно знаем, как будет вызываться обратный вызов, но если он передается произвольной реализации абстракции, иногда мы можем быть ограничены). Для очень горячих путей даже дополнительные затраты на проверку SynchronizationContext и TaskScheduler(обе из которых включают доступ к статике потока) могут добавить измеримые накладные расходы. Если код после a await на самом деле не требует запуска в исходном контексте, использование ConfigureAwait(false) может избежать всех этих затрат: ему не нужно будет стоять в очереди без необходимости, он может использовать все возможные оптимизации и избежать ненужной статики потока.
Избегание тупиков. Рассмотрим библиотечный метод, который использует await результат некоторой сетевой загрузки. Вы вызываете этот метод и синхронно блокируете ожидание его завершения, например, используя .Wait() или .Result отключая .GetAwaiter().GetResult() возвращаемый Task объект. Теперь подумайте, что произойдет, если ваш вызов произойдет, когда текущий SynchronizationContext будет таким, который ограничивает количество операций, которые могут выполняться над ним, до 1, будь то явно через что-то вроде того, что MaxConcurrencySynchronizationContext показано ранее, или неявно, поскольку это контекст, который имеет только один поток, который можно использовать, например поток пользовательского интерфейса. Таким образом, вы вызываете метод в этом одном потоке, а затем блокируете его, ожидая завершения операции. Операция запускает сетевую загрузку и ожидает ее. Поскольку по умолчанию ожидание Task захватывает текущий объект SynchronizationContext, он так и делает, и когда загрузка по сети завершается, он возвращается в очередь к SynchronizationContext обратному вызову, который вызовет оставшуюся часть операции. Но единственный поток, который может обработать поставленный в очередь обратный вызов, в настоящее время заблокирован вашим кодом, ожидающим завершения операции. И эта операция не завершится, пока не будет обработан обратный вызов. Тупик! Это может применяться даже тогда, когда контекст не ограничивает параллелизм всего лишь 1, но когда ресурсы каким-либо образом ограничены. Представьте себе ту же ситуацию, за исключением использования MaxConcurrencySynchronizationContext с ограничением в 4. И вместо того, чтобы делать только один вызов операции, мы ставим в очередь к этому контексту 4 вызова, каждый из которых выполняет вызов и блокирует ожидание его завершения. Сейчас мы все еще заблокировали все ресурсы, ожидая завершения асинхронных методов, и единственное, что позволит этим асинхронным методам завершиться, — это если их обратные вызовы могут быть обработаны этим контекстом, который уже полностью использован. Опять тупик! Если бы вместо этого библиотечный метод использовал ConfigureAwait(false), он не помещал бы обратный вызов в очередь обратно в исходный контекст, избегая сценариев взаимоблокировки.
Зачем мне использовать ConfigurationAwait(true)?
Вы бы этого не сделали, если только вы не использовали его просто как указание на то, что вы намеренно не используете ConfigureAwait(false)(например, чтобы отключить предупреждения статического анализа или тому подобное). ConfigureAwait(true) не делает ничего значимого. По сравнению await taskс await task.ConfigureAwait(true), они функционально идентичны. Если вы видите это ConfigureAwait(true)в рабочем коде, вы можете удалить его без каких-либо негативных последствий.
Этот ConfigureAwait метод принимает логическое значение, поскольку в некоторых нишевых ситуациях необходимо передать переменную для управления конфигурацией. Но в 99% случаев используется жестко закодированное значение ложного аргумента ConfigureAwait(false).
Когда мне следует использовать ConfigurationAwait(false)?
Это зависит от того, реализуете ли вы код уровня приложения или код библиотеки общего назначения?
При написании приложений обычно требуется поведение по умолчанию (именно поэтому оно является поведением по умолчанию). Если модель/среда приложения (например, Windows Forms, WPF, ASP.NET Core и т. д.) публикует пользовательский файл SynchronizationContext, почти наверняка для этого есть веская причина: он предоставляет коду, который заботится о контексте синхронизации, возможность взаимодействовать с моделью/средой приложения соответствующим образом. Итак, если вы пишете обработчик событий в приложении Windows Forms, пишете модульный тест в xunit, пишете код в контроллере ASP.NET MVC, независимо от того, действительно ли модель приложения опубликовала объект SynchronizationContext, вы хотите использовать его, SynchronizationContext если это существует. И это означает, что по умолчанию / ConfigureAwait(true). Вы просто используете await, и правильные вещи происходят в отношении обратных вызовов/продолжений, отправляемых обратно в исходный контекст, если таковой существует. Отсюда следует общее правило: если вы пишете код уровня приложения, не используйтеConfigureAwait(false) . Если вы вспомните пример кода обработчика событий Click ранее в этом посте:
настройку downloadBtn.Content = text необходимо выполнить обратно в исходный контекст. Если код нарушил это правило и вместо этого использовался ConfigureAwait(false) там, где его не следовало:
результатом будет плохое поведение. То же самое можно сказать и о коде классического приложения ASP.NET, использующего HttpContext.Current; использование ConfigureAwait(false), а затем попытка использования, HttpContext.Current скорее всего, приведет к проблемам.
Напротив, библиотеки общего назначения являются «универсальными» отчасти потому, что им не важна среда, в которой они используются. Вы можете использовать их из веб-приложения, из клиентского приложения или из теста, это не имеет значения, поскольку код библиотеки не зависит от модели приложения, в которой он может использоваться. делать что-либо, что должно взаимодействовать с моделью приложения определенным образом, например, он не будет получать доступ к элементам управления пользовательского интерфейса, поскольку библиотека общего назначения ничего не знает об элементах управления пользовательского интерфейса. Поскольку тогда нам не нужно запускать код в какой-либо конкретной среде, мы можем избежать принудительного возврата продолжений/обратных вызовов в исходный контекст, и мы делаем это, используя ConfigureAwait(false) получая как преимущества в производительности, так и в надежности. Это приводит к общему руководству: если вы пишете код библиотеки общего назначения, используйте ConfigureAwait(false) . Вот почему, например, вы увидите каждый (или почти каждый) await в библиотеках времени выполнения .NET Core, использующих ConfigureAwait(false) за некоторыми исключениями, в тех случаях, когда это не так, скорее всего, это ошибка, которую нужно исправить. Например, этот PR исправил пропущенный ConfigureAwait(false) вызов в HttpClient.
Как и в любом руководстве, конечно, могут быть исключения, места, где это не имеет смысла. Например, одно из наиболее серьезных исключений (или, по крайней мере, категорий, требующих размышления) в библиотеках общего назначения — это когда в этих библиотеках есть API, которые требуют вызова делегатов. В таких случаях вызывающая сторона библиотеки потенциально передает код уровня приложения, который может быть вызван библиотекой, что затем фактически делает эти предположения «общего назначения» библиотеки спорными. Рассмотрим, например, асинхронную версию метода Where LINQ, например public static async IAsyncEnumerable<T> WhereAsync(this IAsyncEnumerable<T> source, Func<T, bool> predicate). Нужно ли у predicate здесь вызывать исходный код SynchronizationContext вызывающего клиента? Это зависит от реализации WhereAsync, и это причина, по которой она может отказаться от использования ConfigureAwait(false).
Даже в этих особых случаях общие рекомендации остаются в силе и являются очень хорошей отправной точкой: используйте, ConfigureAwait(false) если вы пишете код библиотеки общего назначения/модели приложения, а в противном случае — нет.
Гарантирует ли ConfigureAwait(false) обратный вызов не будет запущен в исходном контексте?
Нет. Это гарантирует, что он не будет поставлен в очередь обратно в исходный контекст… но это не означает, что код после a await task.ConfigureAwait(false) больше не будет выполняться в исходном контексте. Это связано с тем, что ожидания для уже завершенных ожидаемых объектов просто продолжают выполняться await синхронно, а не заставляют что-либо ставиться обратно в очередь. Таким образом, если вы выполняете await задачу, которая уже завершена к моменту ее ожидания, независимо от того, использовали ли вы ConfigureAwait(false), код сразу после этого продолжит выполнение в текущем потоке в любом контексте, который все еще является текущим.
Можно ли использовать ConfigurationAwait(false) только для первого ожидания в моем методе, а не для остальных?
В общем, нет. См. предыдущий FAQ. Если await task.ConfigureAwait(false) включает в себя задачу, которая уже завершена к моменту ее ожидания (что на самом деле невероятно распространено), то это ConfigureAwait(false) будет бессмысленно, поскольку поток продолжает выполнять код в методе после этого и все еще в том же контексте, который был там ранее.
Одним заметным исключением из этого правила является случай, когда вы знаете, что первый await всегда будет выполняться асинхронно, а ожидаемая вещь будет вызывать обратный вызов в среде, свободной от пользовательского SynchronizationContext или TaskScheduler. Например, CryptoStream в библиотеках времени выполнения .NET необходимо гарантировать, что их потенциально ресурсоемкий код не будет выполняться как часть синхронного вызова вызывающей стороны, поэтому он использует собственный ожидающий элемент , чтобы гарантировать, что все, что происходит после первого, awaitвыполняется в потоке пула потоков. . Однако даже в этом случае вы заметите, что следующий await по-прежнему использует ConfigureAwait(false); технически в этом нет необходимости, но это значительно упрощает проверку кода, поскольку в противном случае каждый раз, когда этот код просматривается, не требуется анализ, чтобы понять, почему ConfigureAwait(false)он был остановлен.
to be continued...