Хабрахабр

[Из песочницы] ConfigureAwait: часто задаваемые вопросы

Привет, Хабр! Представляю вашему вниманию перевод статьи «ConfigureAwait FAQ» автора Стивен Тауб.

image

NET более семи лет назад. Async/await добавили в . NET — оно также находит отражение во многих других языках и фреймворках. Это решение оказало существенное влияние не только на экосистему . NET с точки зрения дополнительных языковых конструкций, использующих асинхронность, реализованы API-интерфейсы с поддержкой асинхронности, произошли фундаментальные улучшения в инфраструктуре, благодаря которым async/await работает как часы (в особенности, улучшены возможности производительности и диагностики в . На данный момент реализовано множество усовершенствований в . NET Core).

Надеюсь, у меня получится ответить на многие из них. ConfigureAwait — один из аспектов async/await, который продолжает вызывать вопросы. Я постараюсь сделать эту статью читаемой от начала до конца, и вместе с тем выполнить ее в стиле ответов на часто задаваемые вопросы (FAQ), чтобы на нее можно было ссылаться в последующем.

Чтобы на самом деле разобраться с ConfigureAwait, мы немного перенесемся назад.

Что такое SynchronizationContext?

Согласно документации System.Threading.SynchronizationContext “Обеспечивает базовую функциональность для распространения контекста синхронизации в различных моделях синхронизации”. Это определение не совсем очевидное.

9% случаев SynchronizationContext используется просто как тип с виртуальным методом Post, который принимает делегат на асинхронное выполнение (в SynchronizationContext есть и другие виртуальные члены, но они встречаются реже и не будут рассмотрены в этой статье). В 99. QueueUserWorkItem для асинхронного выполнения предоставленного делегата. Метод Post базового типа буквально просто вызывает ThreadPool. Производные типы переопределяют Post, чтобы делегат можно было выполнить в нужном месте в нужное время.

BeginInvoke. К примеру, в Windows Forms есть производный от SynchronizationContext тип, который переопределяет Post, чтобы сделать эквивалент Control. В основе Windows Forms лежит обработка сообщений Win32. Это означает, что любой вызов данного Post-метода будет приводить к вызову делегата на более позднем этапе в потоке, связанном с соответствующим Control — так называемом UI потоке. Эти сообщения вызываются движением мыши, кликом, вводом с клавиатуры, системными событиями, доступными для выполнения делегатами и т. Цикл сообщений выполняется в UI потоке, который просто ждет новые сообщения для обработки. Таким образом, при наличии экземпляра SynchronizationContext для UI потока в приложении Windows Forms, чтобы выполнить в нем операцию необходимо передать делегат методу Post. д.

BeginInvoke), при этом управление происходит Диспетчером WPF, а не Windows Forms Control. В Windows Presentation Foundation (WPF) также есть производный от SynchronizationContext тип с переопределенным методом Post, который аналогично “направляет” делегат в UI поток (с помощью Dispatcher.

И в Windows RunTime (WinRT) есть свой SynchronizationContext -производный тип, который также ставит делегат в очередь UI-потока при помощи CoreDispatcher.

Можно также реализовать свой SynchronizationContext с методом Post и какой-нибудь реализацией. Вот что скрывается за фразой “выполнить делегат в UI потоке”. Можно реализовать специальный SynchronizationContext таким образом: Например, я могу не беспокоиться в каком потоке выполняется делегат, но я хочу быть уверен, что любые делегаты метода Post в моем SynchronizationContext выполняются с некоторой ограниченной степенью параллелизма.

internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext
finally { _semaphore.Release(); } }, default, TaskContinuationOptions.None, TaskScheduler.Default); public override void Send(SendOrPostCallback d, object state) { _semaphore.Wait(); try { d(state); } finally { _semaphore.Release(); } }
}

Во фреймворке xUnit есть похожая реализация SynchronizationContext. Здесь она используется для снижения количества кода, связанного с параллельными тестами.

Допустим, я пишу библиотеку, где мне нужно сделать некоторую работу, а затем поставить делегат в очередь обратно в исходный контекст. Преимущество здесь такие же, как и с любой абстракцией: предоставляется единый API, который можно использовать для постановки в очередь на выполнение делегата таким образом, как того пожелает программист, при этом нет необходимости знать детали реализации. Мне не нужно знать, что для Windows Forms нужно взять Control и использовать его BeginInvoke, для WPF использовать BeginInvoke у Dispatcher, или каким-то образом получить контекст и его очередь для xUnit. Для этого мне нужно захватить его SynchronizationContext, и когда я завершу необходимое, мне останется вызвать метод Post данного контекста и передать ему делегат на выполнение. Для этого у SynchronizationContext есть свойство Current. Все что мне нужно — это захватить текущий SynchronizationContext и использовать его позже. Это можно реализовать следующим образом:

public void DoWork(Action worker, Action completion)
{ SynchronizationContext sc = SynchronizationContext.Current; ThreadPool.QueueUserWorkItem(_ => { try { worker(); } finally { sc.Post(_ => completion(), null); } });
}

Установить специальный контекст из свойства Current можно при помощи метода SynchronizationContext.SetSynchronizationContext.

Что такое Планировщик Задач?

SynchronizationContext это общая абстракция для “планировщика”. В некоторых фреймворках для него реализованы собственные абстракции, и System.Threading.Tasks не исключение. Когда в Task есть делегаты, которые могут быть поставлены в очередь и выполнены, они связаны с System.Threading.Tasks.TaskScheduler. Здесь также есть виртуальный метод Post для постановки делегата в очередь на выполнение (вызов делегата реализован при помощи стандартных механизмов), TaskScheduler предоставляет абстрактный метод QueueTask (вызов задачи реализован с помощью метода ExecuteTask).

Default представляет собой пул потоков. Планировщик по умолчанию, который возвращает TaskScheduler. Например, основные библиотеки включают тип System. Из TaskScheduler также есть возможность получить и переопределить методы для настройки времени и места вызова Task. Tasks. Threading. Экземпляр этого класса предоставляет два свойства TaskScheduler: ExclusiveScheduler и ConcurrentScheduler. ConcurrentExclusiveSchedulerPair. Ни одна задача ConcurrentScheduler не будет выполняться, если выполняется задача в ExclusiveScheduler и разрешено запускать одновременно только одну эксклюзивную задачу. Задачи, запланированные в ConcurrentScheduler, могут выполняться параллельно, но с учетом ограничения, задаваемого ConcurrentExclusiveSchedulerPair при его создании (аналогично MaxConcurrencySynchronizationContext). Данное поведение очень похоже на блокировку чтения/записи.

Однако в отличие от SynchronizationContext в нем отсутствует метод для установки текущего планировщика. Как и SynchronizationContext, TaskScheduler имеет свойство Current, которое возвращает текущий TaskScheduler. Так, например, данная программа выведет True, так как лямбда, используемая в StartNew, выполняется в ExclusiveScheduler экземпляра ConcurrentExclusiveSchedulerPair, и TaskScheduler. Вместо этого, планировщик связан с текущей задачей Task. Current установлен на данный планировщик:

using System;
using System.Threading.Tasks; class Program
{ static void Main() { var cesp = new ConcurrentExclusiveSchedulerPair(); Task.Factory.StartNew(() => { Console.WriteLine(TaskScheduler.Current == cesp.ExclusiveScheduler); }, default, TaskCreationOptions.None, cesp.ExclusiveScheduler).Wait(); }
}

Интересно, что TaskScheduler предоставляет статический метод FromCurrentSynchronizationContext. Метод создает новый TaskScheduler и тот ставит задачи в очередь на выполнение в возвращаемом SynchronizationContext.Current контексте, используя метод Post.

Как SynchronizationContext и TaskScheduler связаны с await?

Допустим, необходимо написать UI приложение с кнопкой. Нажатие кнопки инициирует скачивание текста с веб сайта и устанавливает его в Content кнопки. Кнопка должны быть доступна только из UI потока, в котором она и находится, поэтому, когда мы успешно загружаем дату и время и хотим разместить их в Content кнопки, нам нужно это сделать из потока, который имеет над ней контроль. Если это условие не будет выполняться, мы получим исключение:

System.InvalidOperationException: 'Вызывающий поток не может получить доступ к этому объекту, поскольку им владеет другой поток.'

Мы можем вручную использовать 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 потоке, так как в случае с вручную реализованной выше версией, по умолчанию ожидание задачи обращается к SynchronizationContext.Current и TaskScheduler.Current. Когда вы «ожидаете» что-либо в C#, компилятор преобразует код для опроса (вызовом метода GetAwaiter) “ожидаемого” (в данном случае Task) для “ожидающего” (TaskAwaiter). “Ожидающий” отвечает за присоединение коллбэка (часто называемого “продолжением”) который осуществляет обратный вызов в конечный автомат по завершении ожидания. Он реализует это, используя тот контекст/планировщик, который захватил во время регистрации коллбэка. Немного оптимизируем и настроим, получится что-то вроде такого:

object scheduler = SynchronizationContext.Current;
if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
{ scheduler = TaskScheduler.Current;
}

Здесь сначала проверяется, задан ли SynchronizationContext, а если нет – существует ли нестандартный TaskScheduler. Если такой есть, то когда коллбэк будет готов к вызову, будет использован захваченный планировщик; если нет — обратный вызов выполнится как часть операции, завершающей ожидаемую задачу.

Что делает ConfigureAwait(false)

Метод ConfigureAwait не является специальным: он не распознается каким-либо особым образом компилятором или средой выполнения. Это обычный метод, который возвращает структуру (ConfiguredTaskAwaitable — оборачивает оригинальную задачу) и принимает булево значение. Не забывайте, что await может использоваться с любым типом, который реализует правильный паттерн. Если возвращается другой тип, это значит, что когда компилятор получает доступ к методу GetAwaiter (часть паттерна) экземпляров, но делает это из типа, возвращенного из ConfigureAwait, а не из задачи напрямую. Это позволяет менять поведение await для этого специального awaiter.

Логика становится примерно такой: Ожидание типа, возвращаемого 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) используется для предотвращения принудительного вызова коллбэка в исходном контексте или планировщике. Это дает нам несколько преимуществ:

Кроме того мы не можем использовать оптимизацию во время выполнения (мы можем оптимизировать больше, когда точно знаем, как именно будет вызван обратный вызов, но если он передан произвольной реализации абстракции, иногда это накладывает ограничения). Улучшение производительности. Существуют накладные расходы постановки обратного вызова в очередь, в отличие просто от вызова, так как для этого требуется дополнительная работа (и, как правило, дополнительная аллокация). Если код после await не требует выполнения в исходном контексте, используя ConfigureAwait(false) можно избежать всех этих расходов, так как он не нуждается в излишней постановке в очередь, может использовать все доступные оптимизации, а также может избежать ненужного доступа к статике потока. Для высоконагруженных участков даже дополнительные затраты на проверку текущего SynchronizationContext и текущего TaskScheduler (оба из которых подразумевают и доступ к статике потоков) могут существенно увеличить накладные расходы.

Вы вызываете этот метод и синхронно блокируетесь, ожидая полного завершения задачи Task, например, с помощью . Предотвращение дедлоков. Рассмотрим библиотечный метод, который использует await для загрузки чего-либо из сети. Result или . Wait() или . GetResult(). GetAwaiter() . Таким образом, вы вызываете метод в единственном потоке, а затем блокируете его, ожидая завершения операции. Теперь рассмотрим, что происходит, если вызов происходит, когда текущий SynchronizationContext ограничивает число операций в нем до 1 явным образом при помощи MaxConcurrencySynchronizationContext, или неявно, если это контекст с единственным потоком для использования, (например потоком UI). По умолчанию ожидание Task захватит текущий SynchronizationContext (так и в этом случае), и по завершении загрузки из сети, оно помещается в очередь обратно в коллбэк SynchronizationContext, который вызовет оставшуюся часть операции. Происходит запуск загрузки по сети и ожидание ее завершения. И эта операция не будет завершена, пока не будет обработан обратный вызов. Но единственный поток, который может обработать обратный вызов в очереди, в настоящее время заблокирован в ожидании завершения операции. Он может произойти даже в том случае, когда контекст не ограничивает параллелизм до 1, но каким-либо образом ограничены ресурсы. Дедлок! Вместо того чтобы выполнить операцию однократно, мы ставим в очередь к контексту 4 вызова. Представьте себе ту же ситуацию, только со значением 4 для MaxConcurrencySynchronizationContext. Все ресурсы теперь заблокированы в ожидании завершения асинхронных методов, и единственное, что позволит их завершить, это если их коллбэки будут обработаны этим контекстом. Каждый вызов производится и происходит блокировка в ожидании его завершения. Снова дедлок. Однако тот уже полностью занят. Если бы вместо этого библиотечный метод использовал ConfigureAwait(false), он не ставил бы обратный вызов в очередь к исходному контексту, что позволило бы избежать сценариев дедлока.

Нужно ли использовать ConfigureAwait (true)?

Нет, за исключением тех случаев, когда нужно явно указать, что вы не используете ConfigureAwait(false) (например, для скрытия предупреждений статического анализа и т.п.). ConfigureAwait(true) не делает ничего значимого. Если сравнить await task и await task.ConfigureAwait(true) — они окажутся функционально идентичны. Таким образом, если в коде присутствует ConfigureAwait(true), его можно удалить без каких-либо негативных последствий.

Но в 99% случаев задается значение false, ConfigureAwait(false). Метод ConfigureAwait принимает логическое значение, так как в некоторых ситуациях ему может потребоваться передача переменной для управления конфигурацией.

Когда использовать ConfigureAwait(false)?

Это зависит от того, реализуете ли вы код уровня приложения или код библиотеки общего назначения.

Если модель приложения/среда (например, Windows Forms, WPF, ASP. При написании приложений обычно требуется некоторое поведение по умолчанию. Например, если вы пишете, обработчик событий в приложении Windows Forms, тест в xUnit, или код в контроллере ASP. NET Core) публикует специальный SynchronizationContext, почти наверняка этому есть веская причина: значит, код позволяет заботиться о контексте синхронизации для правильного взаимодействия с моделью приложения/средой. Это значит, если используются ConfigureAwait(true) и await, обратные вызовы/продолжения отправляются обратно в исходный контекст — все идет как нужно. NET MVC, независимо от того, опубликовала ли модель приложения SynchronizationContext, вам нужно использовать SynchronizationContext при его наличии. Давайте вернемся к обработчику клика: Отсюда можно сформулировать общее правило: если вы пишете код уровня приложения, не используйте 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"); 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); // баг downloadBtn.Content = text;
}

это приведет к неправильному поведению. То же самое относится и к коду в классическом ASP.NET приложении, зависящем от HttpContext.Current. При использовании ConfigureAwait(false) последующая попытка использовать функцию Context.Current, скорее всего, приведет к проблемам.

Они являются универсальными отчасти потому, что их не волнует среда, в которой они используются. Этим и отличаются библиотеки общего назначения. Агностический также означает, что библиотека не будет делать что-либо для взаимодействия с моделью приложения, например, она не будет получать доступ к элементам управления пользовательского интерфейса, потому что библиотека общего назначения ничего о них не знает. Вы можете использовать их из веб-приложения, из клиентского приложения или из теста — это не имеет значения, так как код библиотеки является агностическим для модели приложения, в которой он может быть использован. Это приводит нас к следующему: если вы пишете код библиотеки общего назначения, используйте ConfigureAwait(false). Так как нет необходимости запускать код в какой-либо конкретной среде, мы можем избежать принудительного вызова продолжений/обратных вызовов к исходному контексту, и мы делаем это, используя ConfigureAwait(false), что дает нам преимущества в производительности и повышает надежность. 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). В таких случаях, библиотека принимает потенциальный код уровня приложения от вызывающей стороны, что делает эти допущения для библиотеки ”общего назначения" весьма спорными. Это зависит от реализации WhereAsync, и это причина, по которой он может решить не использовать ConfigureAwait(false). Должен ли predicate вызываться в исходном SynchronizationContext вызывающего кода?

Даже в особых случаях придерживайтесь общей рекомендации: используйте ConfigureAwait(false) если вы пишете библиотеку общего назначения/app-model-agnostic код.

Гарантирует ли ConfigureAwait (false), что обратный вызов не будет выполнен в исходном контексте?

Нет, это гарантирует, что он не будет поставлен в очередь к исходному контексту. Но это не значит, что код после await не будет выполняться в исходном контексте. Это связано с тем, что уже завершенные операции возвращаются синхронно, а не возвращаются принудительно в очередь. Поэтому, если вы ожидаете задание, которое уже завершено к моменту ожидания, независимо от того, используется ли ConfigureAwait(false), код сразу после этого будет продолжать выполняться в текущем потоке в контексте, который все еще является действительным.

Можно ли использовать ConfigureAwait (false) только при первом ожидании в моем методе, а в остальных — нет?

В общем, нет. Вспомните предыдущий FAQ. Если await task.ConfigureAwait(false) включает задачу, которая уже выполнена к моменту ожидания (что на самом деле происходит довольно часто), тогда использование ConfigureAwait(false) будет бессмысленным, так как поток продолжает выполнять следующий код в методе и по-прежнему в том же контексте, что и был ранее.

Например, CryptoStream в библиотеках среды выполнения . Одно примечательное исключений в том, что первый await всегда будет завершаться асинхронно, и ожидаемая операция вызовет его обратный вызов в среде, свободной от специального SynchronizationContext или TaskScheduler. Для этого он использует специальный awaiter, чтобы убедиться, что код после первого ожидания выполняется в потоке пула потоков. NET проверяет, что его потенциально интенсивный с точки зрения вычислений код не выполняется как часть синхронного вызова вызывающего кода. Однако даже в этом случае можно заметить, что следующий await по-прежнему использует ConfigureAwait(false); Технически в этом нет необходимости, но это значительно упрощает ревью кода, так как не нужно разбираться, почему не был использован ConfigureAwait(false).

Можно ли использовать Task.Run, чтобы избежать использования ConfigureAwait (false)?

Да, если вы напишете:

Task.Run(async delegate
{ await SomethingAsync(); // не увидит оригинальный контекст
});

тогда ConfigureAwait(false) в SomethingAsync() будет лишним, так как делегат, переданный в Task.Run будет выполнен в потоке пула потоков, так что без изменений в коде выше, SynchronizationContext.Current вернет значение null. Более того, Task.Run неявно использует TaskScheduler.Default, поэтому TaskScheduler.Current внутри делегата также вернет значение Default. Это значит, что await будет иметь такое же поведение независимо от того, был ли использован ConfigureAwait(false). Это также не может дать гарантии насчет того, что может делать код внутри данной лямбды. Если у вас есть код:

Task.Run(async delegate
{ SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx()); await SomethingAsync(); // будет нацелен на SomeCoolSyncCtx
});

тогда код внутри SomethingAsync фактически увидит SynchronizationContext.Current экземпляра SomeCoolSyncCtx . и этот await, и любые не настроенные ожидания внутри SomethingAsync будут возвращены в данный контекст. Таким образом, чтобы использовать этот подход, необходимо понимать, что может делать или не делать весь код, который вы ставите в очередь, и могут ли его действия стать помехой.

Это может иметь или не иметь значение для приложения/библиотеки в зависимости от требований к производительности. Этот подход также происходит за счет необходимости создания/постановки в очередь дополнительного объекта задачи.

Например, некоторые инструменты статического анализа помечают флагом ожидания, которые не используют ConfigureAwait(false) CA2007. Также имейте в виду, что такие обходные пути могут вызвать больше проблем, чем преимуществ, и иметь разные непреднамеренные последствия. Это может повлечь за собой еще больше работы, например, вы можете захотеть отключить анализатор из-за его назойливости, а это уже повлечет за собой пропуск других мест в кодовой базе, где на самом деле нужно использовать ConfigureAwait(false). Если вы включите анализатор, а затем используете такой трюк чтобы избежать использования ConfigureAwait, есть большая вероятность, что анализатор отметит его.

Можно ли использовать SynchronizationContext.SetSynchronizationContext, чтобы избежать использования ConfigureAwait (false)?

Нет. Хотя, возможно. Это зависит от используемой реализации

Некоторые разработчики делают так:

Task t;
SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{ t = CallCodeThatUsesAwaitAsync(); // await'ы здесь не увидят оригинальный контекст
}
finally { SynchronizationContext.SetSynchronizationContext(old); }
await t; // будет по-прежнему нацелен на исходный контекст

Так и будет. в надежде, что это заставит код внутри CallCodeThatUsesAwaitAsync рассматривать текущий контекст как null. Current. Однако этот вариант не повлияет на то, какой await видит TaskScheduler. Поэтому если код выполняется в специальном TaskScheduler, await’ы внутри CallCodeThatUsesAwaitAsync будут видеть и становиться в очередь к этому специальному TaskScheduler.

Run FAQ, здесь применимы все те же оговорки: есть определенные последствия такого подхода, и код внутри блока try может также помешать этим попыткам, задав другой контекст (или вызывая код с помощью нестандартного планировщика задач). Как и в Task.

При таком шаблоне также нужно быть осторожным с незначительными изменениями:

SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
try
{ await t;
}
finally { SynchronizationContext.SetSynchronizationContext(old); }

Видите в чем проблема? Немного трудно заметить, но это впечатляет. Нет гарантии, что ожидание в итоге вызовет обратный вызов/продолжение в исходном потоке. Это значит, что возврат SynchronizationContext к исходному может не произойти в первоначальном потоке, что может привести к тому, что последующие рабочие элементы в этом потоке увидят неправильный контекст. Для противодействия этому, хорошо написанные модели приложений, которые задают специальный контекст, как правило, добавляют код для ручного сброса его перед вызовом любого дополнительного пользовательского кода. И даже если это происходит в одном потоке, может понадобиться некоторое время, в течение которого контекст может не быть соответствующим образом восстановлен. А если он работает в ином потоке, это может привести к установке неправильного контекста. И так далее. Довольно далеко от идеала.

Нужно ли использовать ConfigureAwait(false) если я использую GetAwaiter ().GetResult ()?

Нет. ConfigureAwait затрагивает только коллбэки. В частности, шаблон awaiter требует, чтобы awaiter’ы предоставляли свойство 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, не означает, что код другого пользователя или библиотеки их не использует. Так что нужно быть в этом уверенным, или хотя бы признать риск, что такой вариант возможен.

Я слышал, что в .NET Core нет необходимости применять ConfigureAwait (false). Так ли это?

Не так. Она необходима при работе в .NET Core по тем же причинам, что и при работе в .NET Framework. В этом плане ничего не изменилось.

В частности, в то время как классический ASP. Изменилось то, публикуют ли определенные среды собственный SynchronizationContext. NET Framework имеет свой SynchronizationContext, у ASP. NET в . Это означает, что код, запущенный в приложении ASP. NET Core его нет. NET Core по умолчанию не будет видеть специальный SynchronizationContext, что уменьшает необходимость в ConfigureAwait(false) в данной среде.

Если какой-либо код пользователя (или другой код библиотеки, используемый приложением) задает пользовательский контекст и вызывает ваш код или вызывает ваш код в Задаче, запланированной в специальном планировщике задач, тогда await’ы в ASP. Однако это не значит, что никогда не будет присутствовать пользовательский SynchronizationContext или TaskScheduler. Конечно, в ситуациях, когда вы избегаете синхронных блокировок (что в любом случае нужно делать в веб-приложениях) и если вы не против небольших накладных расходов в производительности в некоторых случаях, вы можете обойтись без использования ConfigureAwait(false). NET Core будут видеть нестандартный контекст или планировщик, который может привести к необходимости использования ConfigureAwait(false).

Могу ли я использовать ConfigureAwait, когда «ожидаю выполнения foreach» над IAsyncEnumerable?

Да. Пример см. в статье MSDN.

Он также может использоваться для перечисления элементов, которые представляют правильную область API. Await foreach соответствует шаблону и, таким образом, может использоваться для перечисления в IAsyncEnumerable<T>. NET включают метод расширения ConfigureAwait для IAsyncEnumerable<T>, который возвращает специальный тип, который оборачивает IAsyncEnumerable<T> и Boolean и соответствует правильному шаблону. Библиотеки времени выполнения . Эти вызовы относятся к возвращенному сконфигурированному типу структуры перечислителя, который в свою очередь, выполняет ожидания нужным образом. Когда компилятор генерирует вызовы к MoveNextAsync и DisposeAsync перечислителя.

Можно ли использовать ConfigureAwait, при ‘await using’ IAsyncDisposable?

Да, пусть и с небольшим усложнением.

NET библиотеки времени выполнения предоставляют метод расширения ConfigureAwait для IAsyncDisposable и await using, будет отлично работать, поскольку он реализует соответствующий шаблон (а именно, предоставляет соответствующий метод DisposeAsync): Как и с IAsyncEnumerable<T>, .

await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
{ ...
}

Проблема здесь состоит в том, что тип c — теперь не 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>) всегда будет проходить через awaits, независимо от того, используется ли ConfigureAwait во избежание захвата исходного SynchronizationContext. Более подробно рассмотрено в этой статье.

Могут ли языковые средства помочь мне избежать необходимости явно использовать ConfigureAwait(false) в моей библиотеке?

Разработчики библиотек иногда выражают недовольство необходимостью использовать ConfigureAwait(false) и просят менее инвазивные альтернативы.

Однако существует множество предложений относительно того, как это можно реализовать, например: 1, 2, 3, 4. В настоящее время их нет, по крайней мере, они не встроены в язык/компилятор/среду выполнения.

Если рассмотренная тема вам интересна, если у вас есть новые и интересные идеи, автор оригинальной статьи приглашает вас к обсуждению.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»