.NET добавил async
/await
к языкам и библиотекам более семи лет назад. В то же время он завоевал популярность не только в экосистеме .NET, но и во множестве других языков и фреймворков. Также было замечено множество улучшений в .NET с точки зрения дополнительных языковых конструкций, использующих асинхронность, API-интерфейсов, предлагающих асинхронную поддержку, и фундаментальных улучшений в инфраструктуре, которые делают async
/await
(в частности, конкретное исполнение и улучшение диагностики в .NET).
Однако один из аспектов async
/await
, который продолжает вызывать вопросы, - это ConfigureAwait. Эта статья послужит общей информацией и списком часто задаваемых вопросов, которые можно использовать в качестве справочного материала в будущем.
Чтобы действительно понять ConfigureAwait, нам нужно начать немного раньше ...
Что такое SynchronizationContext?
System
.
Threading
.
SynchronizationContext
docs утверждают, что он «обеспечивает основные функциональные возможности для распространения контекста синхронизации в различных моделях синхронизации». Не совсем очевидное описание.
В 99,9% случав использования SynchronizationContext
- это просто тип, который предоставляющий виртуальный метод Post
, который делегирует асинхронное выполнение (существует множество других виртуальных членов в SynchronizationContext, (они менее используемые). Post
базового типа, буквально вызывает ThreadPool.QueueUserWorkItem для асинхронного вызова предоставленного делегата. Однако производные типы переопределяют Post
, чтобы разрешить выполнение этого делегата в наиболее подходящем месте и в наиболее подходящее время.
Например, Windows Forms имеет производный от SynchronizationContext тип, который переопределяет Post
, чтобы сделать эквивалент Control.BeginInvoke; это означает, что делегат будет вызван в какой-то более поздний момент в потоке, связанном с этим соответствующим элементом управления, иначе говоря, «the UI thread». Windows Forms полагается на обработку сообщений Win32 и имеет «message loop», работающий в потоке пользовательского интерфейса, который просто ожидает поступления новых сообщений для обработки. Эти сообщения могут быть для движений и щелчков мыши, для ввода с клавиатуры, для системных событий, для делегатов, доступных для вызова и т. Д. Таким образом, с учетом экземпляра SynchronizationContext для потока пользовательского интерфейса приложений Windows Forms, чтобы получить делегата для выполнения на этот поток пользовательского интерфейса, просто нужно передать его в Post.
То же самое касается Windows Presentation Foundation (WPF). Он имеет свой собственный производный от SynchronizationContext тип с переопределением Post
, которое аналогично «marshals» делегат в потоке пользовательского интерфейса (через Dispatcher.BeginInvoke), в данном случае управляемый диспетчером WPF, а не элементом управления Windows Forms.
И для Windows RunTime (WinRT). Он имеет свой собственный производный от SynchronizationContext тип с переопределением Post
, который также ставит делегата в очередь потока пользовательского интерфейса через его CoreDispatcher.
Это выходит за рамки простого «run this delegate on the UI thread». Любой может реализовать SynchronizationContext
с Post
, который делает все что угодно. Например, мне может быть все равно, в каком потоке работает делегат, но я хочу убедиться, что все делегаты Post
моего SynchronizationContext
выполняются с некоторой ограниченной степенью параллелизма. Я могу добиться этого с помощью специального SynchronizationContext, например:
Фактически, среда модульного тестирования xunit предоставляет SynchronizationContext, очень похожий на этот, который он использует для ограничения объема кода, связанного с тестами, которые могут выполняться одновременно.
Преимущество является в том, что, как и с любой абстракцией: он предоставляет единый API, который можно использовать для постановки в очередь делегата для обработки, как этого захочет создатель реализации. Итак, если я пишу библиотеку, и я хочу уйти и выполнить некоторую работу, а затем поставить делегата в очередь обратно в «context» исходного местоположения, мне просто нужно взять их SynchronizationContext, удержать его, а затем когда закончите свою работу, вызовите Post
в этом контексте, чтобы передать делегата. Мне не нужно знать, что для Windows Forms я должен взять Control
и использовать его BeginInvoke, или для WPF я должен взять Dispatcher
и использовать его BeginInvoke, или для xunit я должен каким-то образом получить его контекст и очередь к нему; Мне просто нужно взять текущий SynchronizationContext
и использовать его позже. Чтобы достичь этого, SynchronizationContext
предоставляет свойство Current
, так что для достижения вышеупомянутой цели я мог бы написать такой код:
Фреймворк, который хочет предоставить пользовательский контекст из Current
, использует метод SynchronizationContext.SetSynchronizationContext.
Что такое TaskScheduler?
Synchronization
Context
- это общая абстракция для «scheduler». Отдельные структуры иногда имеют свои собственные абстракции для планировщика, и System.Threading.Tasks не является исключением. Когда задачи поддерживаются делегатом, так что они могут быть поставлены в очередь и выполнены, они связаны с System.Threading.Tasks.TaskScheduler. Так же, как SynchronizationContext
предоставляет виртуальный метод Post
для постановки в очередь вызова делегата (с реализацией, позже вызывающей делегат через типичные механизмы вызова делегата), TaskScheduler
предоставляет абстрактный метод QueueTask
(с реализацией, позже вызывающей эту задачу через метод ExecuteTask
).
Планировщик по умолчанию, возвращаемый TaskScheduler.Default, является пулом потоков, но его можно извлечь из TaskScheduler
и переопределить соответствующие методы для достижения произвольного поведения, когда и где вызывается Task
. Например, основные библиотеки включают
Тип System.Threading.Tasks.ConcurrentExclusiveSchedulerPair. Экземпляр этого класса предоставляет два свойства TaskScheduler
, одно из которых называется ExclusiveScheduler
, а другое - ConcurrentScheduler. Задачи, запланированные для ConcurrentScheduler
, могут выполняться одновременно, но при условии ограничения, предоставленного ConcurrentExclusiveSchedulerPair
при его создании (аналогично MaxConcurrencySynchronizationContext
, показанного ранее), и никакие задачи ConcurrentScheduler
не будут выполняться, когда выполняется задача, запланированная для ExclusiveScheduler
.
Как и SynchronizationContext, TaskScheduler
также имеет свойство Current
, которое возвращает «current» TaskScheduler. Однако, в отличие от SynchronizationContext, здесь нет способа настроить текущего планировщика. Вместо этого текущий планировщик - тот, который связан с текущей выполняющимся Task, и планировщик предоставляется системе как часть запуска Task. Так, например, эта программа выведет «True», так как лямбда, используемая с StartNew
, выполняется в ConcurrentExclusiveSchedulerPair
ExclusiveScheduler
и увидит TaskScheduler.Current, установленный в этот планировщик:
Интересно, что TaskScheduler
предоставляет статический метод FromCurrentSynchronizationContext
, который создает новый TaskScheduler
, который ставит в очередь Task
s для выполнения на любом возвращаемом SynchronizationContext.Current, используя метод Post
для постановки в очередь задач.
Как SynchronizationContext и TaskScheduler связаны с ожиданием?
Подумайте о написании приложения UI с помощью Button. П Нажав на Button, мы хотим загрузить текст с веб-сайта и установить его в качестве Button
Content
. Доступ к Button возможен только из потока UI, которому она принадлежит, когда мы успешно загрузили новый период, временный текст и хотим сохранить его обратно в Button
‘s Content
нам нужно сделать это из потока, которому принадлежит элемент управления. Если мы этого не сделаем, мы получим отклонение, например:
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
Если бы мы писали это вручную, мы могли бы использовать SynchronizationContext
, как показано ранее, чтобы перенаправить настройку Content
обратно в исходный контекст, например через TaskScheduler:
private static readonly HttpClient s_httpClient = new HttpClient();
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
{
downloadBtn.Content = downloadTask.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());
}
или используя SynchronizationContext
напрямую:
private static readonly HttpClient s_httpClient = new HttpClient();
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
SynchronizationContext sc = SynchronizationContext.Current;
s_httpClient.GetStringAsync("http://example.com/currenttime").ContinueWith(downloadTask =>
{
sc.Post(delegate
{
downloadBtn.Content = downloadTask.Result;
}, null);
});
}
Оба этих подхода, тем не менее, явно используют обратные вызовы. Вместо этого мы хотели бы написать код естественным образом с помощью async
/await
:
private static readonly HttpClient s_httpClient = new HttpClient();
private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
downloadBtn.Content = text;
}
Это «просто работает», при успешной установке Content
в потоке UI, потому что, как и в случае с ручной реализованной версией, await
Task
по умолчанию обращает внимание на SynchronizationContext.Current, а также TaskScheduler.Current
. Когда вы await
чего-либо в C #, компилятор преобразует код для запроса (посредством вызова GetAwaiter) «ожидаемого» (в данном случае, Task) для «ожидающего» (в данном случае TaskAwaiter<string>). Этот ожидающий отвечает за подключение обратного вызова (часто называемого «продолжением»), который будет вызывать обратный вызов в конечный автомат после завершения ожидаемого объекта, и он делает это с использованием любого контекста / планировщика, захваченного во время обратного вызова. Хотя не совсем используемый код (используются дополнительные оптимизации и настройки), он выглядит примерно так:
object scheduler = SynchronizationContext.Current;
if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
{
scheduler = TaskScheduler.Current;
}
Другими словами, он сначала проверяет, установлен ли SynchronizationContext, если его нету, независимо от запуска TaskScheduler
выполняется по умолчанию. Если он сочтет, что обратный вызов готов к запуску он будет использовать фиксированнй планировщик; в противном случае он обычно выполняет обратный вызов как часть операции, завершающей ожидаемую задачу.
Что делает ConfigureAwait (false)?
Метод ConfigureAwait
не является особенным: он не распознается каким-либо особым компилятором или средой выполнения. Это просто метод, который возвращает структуру (ConfiguredTaskAwaitable), которая возвращает исходную задачу, к которой она была вызвана, а также указанное логическое значение. Помните, что await
может использоваться с любым типом, который выставляет правильный паттерн. Возвращая другой тип, это означает что когда компилятор получает доступ к методу GetAwaiter
(часть паттерна), он делает это вне типа, непосредственно из задачи.
В частности, ожидание типа, возвращенного из ConfigureAwait(continueOnCapturedContext: false) вместо ожидания Task
, напрямую заканчивается воздействием на логику, показанную ранее для того, как захватывается целевой контекст / планировщик. Это эффективно делает ранее показанную логику более похожей на это:
object scheduler = null;
if (continueOnCapturedContext)
{
scheduler = SynchronizationContext.Current;
if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
{
scheduler = TaskScheduler.Current;
}
}
Другими словами, указав значение false, даже если существует текущий контекст или планировщик для обратного вызова, он делает вид, что его нет.
Почему нужно использовать ConfigureAwait (false)?
ConfigureAwait(continueOnCapturedContext: false) используется, чтобы не вызывать обратный вызов в исходном контексте или планировщике. Это имеет несколько преимуществ:
Улучшение производительности. Стоит поставить в очередь обратный вызов вместо того, чтобы просто вызывать его, потому что это связано с дополнительной работой (и, как правило, с дополнительным распределением), а также с тем, что некоторая оптимизация, которую мы иначе хотели бы использовать во время выполнения.
Для очень срочных путей даже дополнительные затраты на проверку текущего SynchronizationContext
и текущего TaskScheduler (оба из которых включают в себя доступ к статическим потокам) могут добавить накладные расходы. Если код после await на самом деле не требует выполнения в исходном контексте, использование ConfigureAwait(false) может избежать всех этих затрат: его не нужно ставить в очередь, он может использовать все возможные оптимизации.
Как избежать тупиков. Рассмотрим библиотечный метод, который использует await в результате некоторой загрузки по сети. Вы вызываете этот метод и синхронно блокируете ожидание его завершения, например, с помощью .Wait()или .Result или .GetAwaiter().GetResult() выключить возвращенный объект Task.
Теперь рассмотрим, что произойдет, если ваш вызов этого произойдет, если текущий SynchronizationContext ограничивает число операций, которые могут быть запущены на нем, до 1, будь то явно с помощью чего-то вроде MaxConcurrencySynchronizationContext, показанного ранее, или неявным образом из-за того, что это контекст, который только имеет одну нить, которую можно использовать, например, поток пользовательского интерфейса. Таким образом, вы вызываете метод в этом одном потоке, а затем блокируете его, ожидая завершения операции. Операция запускает загрузку по сети и ожидает ее. Так как ожидание Задачи по умолчанию захватывает текущий SynchronizationContext, он делает это, и когда загрузка по сети завершается, он помещает обратно в SynchronizationContext обратный вызов, который вызовет оставшуюся часть операции. Но единственный поток, который может обработать обратный вызов в очереди, в настоящее время заблокирован блокировкой кода, ожидающей завершения операции. И эта операция не завершится, пока не будет обработан обратный вызов. Тупик! Это может применяться, даже когда контекст не ограничивает параллелизм только 1, но когда ресурсы ограничены. Представьте себе ту же ситуацию, за исключением использования MaxConcurrencySynchronizationContext с лимитом 4. И вместо того, чтобы делать только один вызов операции, мы ставим в очередь в этот контекст 4 вызова, каждый из которых делает вызов и блокирует ожидание его завершения. Теперь мы все еще заблокировали все ресурсы в ожидании завершения асинхронных методов, и единственное, что позволит этим асинхронным методам завершиться, - это если их обратные вызовы могут быть обработаны этим контекстом, который уже полностью используется. Опять тупик! Если бы вместо этого метод библиотеки использовал ConfigureAwait(false), он не поставил бы в очередь обратный вызов обратно в исходный контекст, избегая сценариев взаимоблокировки.
Почему я использую ConfigureAwait (true)?
ConfigureAwait(true) не делает ничего значимого. При сравнении await task с await task.ConfigureAwait(true). ConfigureAwait (true) они функционально идентичны. Если вы видите ConfigureAwait(true) в рабочем коде, вы можете удалить его без вреда для себя.
Метод ConfigureAwait принимает логическое значение, поскольку существуют некоторые нишевые ситуации, в которых вы хотите передать переменную для управления конфигурацией. Но в 99% случаев используется жестко закодированное значение ложного аргумента ConfigureAwait(false).
В каких случаях использовать ConfigureAwait (false)?
Это зависит от того, реализуете ли вы код уровня приложения или код библиотеки общего назначения?
При написании приложений вы обычно хотите действий по умолчанию. Если модель / среда приложения (например, Windows Forms, WPF, ASP.NET Core и т. Д.) Публикует пользовательский SynchronizationContext, почти наверняка есть веская причина для этого: он предоставляет способ для кода, который заботится о контексте синхронизации, взаимодействовать с моделью приложения / среды соответственно. Поэтому, если вы пишете обработчик событий в приложении Windows Forms, пишете модульный тест в xunit, пишете код в контроллере ASP.NET MVC, независимо от того, действительно ли модель приложения опубликовала SynchronizationContext, вы хотите использовать SynchronizationContext, если он существует. А это означает, что по умолчанию / ConfigureAwait(true). Вы просто используете await, и правильное осуществление происходит в отношении обратных вызовов / продолжений, отправляемых обратно в исходный контекст, если таковой существует. Это приводит к общему руководству: если вы пишете код уровня приложения, не используйте ConfigureAwait
(
false
)
.
Если вы вспомните пример кода обработчика события Click ранее в этом посте:
private static readonly HttpClient s_httpClient = new HttpClient();
private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
string text = await s_httpClient.GetStringAsync("http://example.com/currenttime");
downloadBtn.Content = text;
}
установка downloadBtn.Content = text должна быть сделана обратно в исходном контексте. Если код нарушил это правило и вместо этого использовал ConfigureAwait(false) тогда он не должен иметь:
private static readonly HttpClient s_httpClient = new HttpClient();
private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
string text = await s_httpClient.GetStringAsync("http://example.com/currenttime").ConfigureAwait(false); // bug
downloadBtn.Content = text;
}
То же самое относится и к коду в классическом приложении ASP.NET, зависящем от HttpContext.Current; использование ConfigureAwait(false), а затем использования HttpContext.Current, вероятно, приведет к проблемам.
В отличие от этого, библиотеки общего назначения являются «универсальными» отчасти потому, что их не волнует среда, в которой они используются. Вы можете использовать их из веб-приложения, из клиентского приложения или из теста, это не имеет значения, поскольку код библиотеки не зависит от модели приложения, в которой он может быть использован. Быть независимым также означает, что он не собирается делать что-то, что должно взаимодействовать с моделью приложения определенным образом, например, он не будет получать доступ к элементам управления UI, потому что библиотека общего назначения ничего не знает об элементах управления UI. Поскольку тогда нам не нужно запускать код в какой-либо конкретной среде, мы можем избежать принудительного возврата продолжений / обратных вызовов к исходному контексту, и мы делаем это, используя ConfigureAwait(false) и получая преимущества как в производительности, так и в надежности. Это приводит к общему руководству: если вы пишете код библиотеки общего назначения, используйте ConfigureAwait
(
false
)
. Вот почему, например, вы увидите каждое (или почти каждое) await
в библиотеках среды выполнения .NET Core, использующее ConfigureAwait(false) в каждом await; за некоторыми исключениями, в случаях, когда это не очень вероятно, ошибка может быть исправлена. Например, этот 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), что обратный вызов не будет выполняться в исходном контексте?
Нет. Это гарантирует, что он не будет поставлен в очередь в исходный контекст ... но это не означает, что код после await task.ConfigureAwait(false) по-прежнему не будет работать в исходном контексте. Это связано с тем, что ожидающие не заставляют что-либо возвращаться в очередь. Таким образом, если вы ожидаете задачу, которая уже выполнена к тому времени, независимо от того, использовали ли вы ConfigureAwait(false), код сразу после этого продолжит выполняться в текущем потоке в любом контексте.
Заметным исключением является то, что вы знаете, что первое await
всегда завершается асинхронно, а ожидаемое событие вызывает свой обратный вызов в среде, свободной от пользовательского SynchronizationContext
или TaskScheduler. Например, CryptoStream
в библиотеках времени выполнения .NET хочет, чтобы его потенциально вычислительно-интенсивный код не выполнялся как часть синхронного вызова вызывающей стороны, чтобы все после первого await
выполнялось в потоке резерва. Однако даже в этом случае вы заметите, что в следующем await
все еще используется ConfigureAwait(false); технически это не является необходимым, но это делает обзор кода намного проще, так как в противном случае каждый раз, когда этот код просматривается, он не требует анализа, чтобы понять, почему ConfigureAwait(false) был отключен.
Можно ли применять Task.Run, чтобы избежать использования ConfigureAwait (false)?
Да. Если вы напишите:
Task.Run(async delegate
{
await SomethingAsync(); // won't see the original context
затем ConfigureAwait(false) для этого вызова SomethingAsync() будет nop, поскольку делегат, переданный в Task.Run, будет выполняться в потоке пула потоков без кода пользователя выше в стеке, например SynchronizationContext.Current вернет null. Кроме того, Task.Run
неявно использует TaskScheduler.Default, что означает, что запрос TaskScheduler.Current
внутри делегата также вернет Default. Это означает, что ожидание будет демонстрировать одинаковое действия независимо от того, использовался ли ConfigureAwait(false). Он также не дает никаких гарантий относительно того, что может делать код внутри этой лямбды. Если у вас есть код:
Task.Run(async delegate
{
SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx());
await SomethingAsync(); // will target SomeCoolSyncCtx
});
код внутри SomethingAsync на самом деле увидит SynchronizationContext.Current как тот экземпляр SomeCoolSyncCtx, и это await
, и любые ненастроенные задачи внутри SomethingAsync отправят обратно на него. Таким образом, чтобы использовать этот подход, вам необходимо понять, что весь код, который вы ставите в очередь, может или не может делать, и могут ли его действия помешать вашему.
Этот подход также достигается за счет необходимости создавать / ставить в очередь дополнительный объект задачи. Это может иметь значения для вашего приложения или библиотеки в зависимости от вашей чувствительности к производительности.
Также имейте в виду, что такие уловки могут вызвать больше проблем, чем они стоят, и иметь другие непредвиденные последствия. Например, инструменты статического анализа (например, анализаторы Roslyn) были написаны для отметки ожидающих, которые не используют ConfigureAwait(false), таких как CA2007. Если вы включили такой анализатор, но затем применили хитрость, подобную этой, просто чтобы избежать использования ConfigureAwait, есть большая вероятность, что анализатор пометит его и фактически вызовет дополнительную работу для вас. Так что, возможно, вы затем отключите анализатор из-за его шумности, и теперь вы в конечном итоге пропустите другие места в кодовой базе, где вы на самом деле должны были использовать ConfigureAwait(false).
Можно ли использовать SynchronizationContext.SetSynchronizationContext, вместо ConfigureAwait (false)?
Это зависит от используемого кода.
Некоторые разработчики пишут такой код:
Task t;
SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
t = CallCodeThatUsesAwaitAsync(); // awaits in here won't see the original context
}
finally { SynchronizationContext.SetSynchronizationContext(old); }
await t; // will still target the original context
в надежде, что он сделает код внутри CallCodeThatUsesAwaitAsync, увидит текущий контекст как null. Однако вышеприведенное не повлияет на ожидаемый файл TaskScheduler.Current, поэтому, если этот код выполняется на каком-то пользовательском TaskScheduler, await
внутри CallCodeThatUsesAwaitAsync (и не использующий ConfigureAwait(false)) все равно увидит и вернется в очередь к этому пользовательскому TaskScheduler.
Все те же предостережения также применимы, как и в предыдущем FAQ, связанном с Task.Run: такие обходные пути имеют практические последствия, и код внутри попытки может также помешать, установив другой контекст (или вызвав код не по умолчанию TaskScheduler).
При таком шаблоне вы также должны быть осторожны с небольшим изменением:
SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{
await t;
}
finally { SynchronizationContext.SetSynchronizationContext(old); }
Нет никакой гарантии, что ожидание закончится вызовом обратного вызова / продолжения в исходном потоке, что означает, что сброс SynchronizationContext обратно к исходному может фактически не произойти в исходном потоке, что может привести к тому, что последующие рабочие элементы в этом потоке будут видны в неправильном контексте (чтобы противостоять этому, хорошо написанные модели приложений, которые устанавливают пользовательский контекст, обычно добавляют код, чтобы сбросить его вручную перед вызовом любого другого пользовательского кода). И даже если он действительно работает в том же потоке, может пройти некоторое время, прежде чем он будет выполнен, так что контекст не будет должным образом восстановлен на некоторое время. И если он работает в другом потоке, это может привести к неправильному контексту в этом потоке. И так далее.
Использование GetAwaiter (). GetResult (). Нужно ли использовать ConfigureAwait (false)?
Нет. ConfigureAwait
влияет только на обратные вызовы. В частности, шаблон ожидающего требует, чтобы ожидающие предоставили свойство IsCompleted
, метод GetResult
и метод OnCompleted
(необязательно с методом UnsafeOnCompleted
). ConfigureAwait
влияет только на поведение {
Unsafe
}
OnCompleted
, поэтому, если вы просто напрямую вызываете GetResult(), независимо от того, делаете ли вы это с помощью TaskAwaiter
или ConfiguredTaskAwaitable.ConfiguredTaskAwaiter. Поэтому, если вы видите в коде task.ConfigureAwait(false). GetAwaiter().GetResult(), вы можете заменить его на task.GetAwaiter().GetResult() (а также подумать, действительно ли вы хотите блокировать подобное).
Я знаю, что работаю в среде, в которой никогда не будет настраиваемого SynchronizationContext или настраиваемого TaskScheduler. Могу ли пропустить, использование ConfigureAwait (false)?
Как упоминалось в предыдущих часто задаваемых вопросах, то, что модель приложения, в которой вы работаете, не устанавливает настраиваемый SynchronizationContext
и не вызывает ваш код в настраиваемом TaskScheduler
, не означает, что какой-то другой пользовательский или библиотечный код этого не делает. Таким образом, вы должны быть уверены, что это не так, или, по крайней мере, учитывать риск.
Я слышал, что ConfigureAwait (false) больше не требуется в .NET Core. Правда?
Ложь. Он необходим при работе в .NET Core по тем же причинам, что и при работе в .NET Framework. В этом отношении ничего не изменилось.
Однако изменилось то, что публикуют определенные среды свой собственный SynchronizationContext. В частности, тогда как классический ASP.NET в .NET Framework имеет свой собственный SynchronizationContext, в отличие от ASP.NET Core нет. Это означает, что код, работающий в приложении ASP.NET Core по умолчанию, не увидит пользовательский SynchronizationContext, что уменьшает необходимость выполнения ConfigureAwait(false) в такой среде.
Однако это не означает, что пользовательский SynchronizationContext
или TaskScheduler
никогда не будет представлен. Если какой-либо пользовательский код (или другой код библиотеки, используемый вашим приложением) устанавливает пользовательский контекст и вызывает ваш код или вызывает ваш код в Task
, запланированной для пользовательского TaskScheduler, то даже в ASP.NET Core ваши ожидающие могут увидеть не контекст по умолчанию или планировщик, который заставит вас хотеть использовать ConfigureAwait(false). Конечно, в таких ситуациях, если вы избегаете синхронной блокировки (что вы должны избегать в веб-приложениях независимо от этого), если вы не возражаете против небольших накладных расходов на производительность в таких ограниченных случаях, вы, вероятно, можете обойтись без использования ConfigureAwait(false).
Могу ли я использовать ConfigureAwait, когда ожидание по каждому элементу в IAsyncEnumerable?
Да. Посмотрите эту статью MSDN Magazine для примера.
await foreach привязывается к шаблону, и поэтому, хотя он может использоваться для перечисления IAsyncEnumerable<T>, он также может использоваться для перечисления чего-то, что предоставляет правильную площадь поверхности API. Библиотеки времени выполнения .NET включают метод расширения ConfigureAwait
для IAsyncEnumerable<T>, который возвращает пользовательский тип, заключающий в себе IAsyncEnumerable<T> и Boolean
, выставляет правильный шаблон. Когда компилятор генерирует вызовы методов MoveNextAsync и DisposeAsync
, эти вызовы возвращаются к сконфигурированному типу структуры, и он, в свою очередь, выполняет ожидание желаемым сконфигурированным способом.
Могу ли я использовать ConfigureAwait, когда «ожидаю использования» IAsyncDisposable?
Да, хотя с небольшими сложностями.
Библиотеки времени выполнения .NET предоставляют метод расширения ConfigureAwait
для IAsyncDisposable, и использование await using будет успешно работать с этим, поскольку он реализует соответствующий шаблон (а именно, предоставляет соответствующий метод DisposeAsync
):
await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
{
...
}
Проблема здесь в том, что тип теперь не MyAsyncDisposableClass
, а скорее System.Runtime.CompilerServices.ConfiguredAsyncDisposable
, который является типом, возвращаемым из этого метода расширения ConfigureAwait
в IAsyncDisposable
.
Чтобы обойти это, вам нужно написать одну дополнительную строку:
var c = new MyAsyncDisposableClass();
await using (c.ConfigureAwait(false))
{
...
}
Теперь тип c снова является желаемым MyAsyncDisposableClass. Это также имеет эффект увеличения области действия c; если это эффективно, вы можете обернуть все это в фигурные скобки.
Я использовал ConfigureAwait (false), но AsyncLocal все еще передавался в код после ожидания. Это ошибка?
Нет, это ожидаемо. Данные AsyncLocal<T> передаются как часть ExecutionContext, которая отделена от SynchronizationContext. Если вы явно не отключили поток ExecutionContext с помощью ExecutionContext.SuppressFlow(), ExecutionContext (и, следовательно, данные AsyncLocal
<
T
>
) всегда будут проходить через await
, независимо от того, используется ли ConfigureAwait
, чтобы избежать захвата исходного SynchronizationContext. Для получения дополнительной информации см. Этот пост в блоге.
Может ли язык помочь мне избежать необходимости использовать ConfigureAwait (false) в моей библиотеке?
Разработчики библиотек иногда выражают недовольство необходимостью использования ConfigureAwait(false) и просят о менее агрессивных альтернативах.
В настоящее время их нет, по крайней мере, они не встроены в язык / компилятор / среду выполнения. Однако существует множество предложений о том, как может выглядеть такое решение, например,
https://github.com/dotnet/csharplang/issues/645,
https://github.com/dotnet/csharplang/issues/2542,
https://github.com/dotnet/csharplang/issues/2649,
https://github.com/dotnet/csharplang/issues/2746.
Если это важно для вас, и вы чувствуете, что у вас есть новые и интересные идеи, можете поделиться своими мыслями.
Источник