.NET Разработчик
6.53K subscribers
442 photos
3 videos
14 files
2.12K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День триста двадцать первый. #AsyncAwaitFAQ
FAQ по async/await и ConfigureAwait
Async/await были добавлены в .NET более 7 лет назад. За это время было выпущено множество улучшений в инфраструктуре, дополнительных языковых конструкций, API-интерфейсов. Однако один из аспектов async/await, который продолжает вызывать вопросы, - это ConfigureAwait. В серии постов #AsyncAwaitFAQ мы ответим на несколько наиболее часто задаваемых вопросов про контекст синхронизации в async/await.
1. Что такое контекст синхронизации?
Документация по System.Threading.SynchronizationContext утверждает, что он «обеспечивает базовую функциональность для распространения контекста синхронизации в различных моделях синхронизации». Не совсем понятное описание.
В 99,9% случаев SynchronizationContext - это тип, который предоставляет виртуальный метод Post, принимающий делегат для асинхронного выполнения (в SynchronizationContext есть множество других виртуальных членов, но они гораздо реже используются). Метод Post базового типа просто вызывает ThreadPool.QueueUserWorkItem для асинхронного вызова предоставленного делегата. Однако производные типы переопределяют метод Post, чтобы разрешить выполнение этого делегата в наиболее подходящем месте и в наиболее подходящее время.
В Windows Forms переопределённый метод Post, создаёт эквивалент Control.BeginInvoke. То есть любые вызовы метода Post приведут к тому, что делегат будет вызван позже в потоке, связанном с этим элементом управления, т.е. в UI-потоке. Windows Forms полагается на обработку сообщений Win32 и имеет «цикл сообщений», работающий в UI-потоке, который просто ожидает поступления новых сообщений для обработки (движения и щелчки мыши, ввод с клавиатур, системные события, делегаты и т. д.). Таким образом, имея экземпляр SynchronizationContext для UI-потока приложения Windows Forms, чтобы получить делегат для выполнения в UI-потоке, просто нужно передать его в Post.
WPF имеет свой собственный производный от SynchronizationContext тип с переопределённым Post, который аналогично «маршализирует» делегат в UI-поток (через Dispatcher.BeginInvoke), в данном случае управляемый диспетчером WPF, а не элементом управления Windows Forms.
В Windows RunTime (WinRT) переопределённый метод Post, также ставит делегат в очередь UI-потока через CoreDispatcher.
Это выходит за рамки простого «запуска этого делегата в UI-потоке». Любой может реализовать SynchronizationContext с методом Post, который делает всё что угодно.
Преимуществом такого подхода в том, что он предоставляет единый API, который можно использовать для постановки в очередь делегата для обработки, как того пожелает создатель реализации, без необходимости знать детали этой реализации. Итак, если я пишу библиотеку, и я хочу асинхронно выполнить некоторую работу, а затем поставить делегат в очередь обратно в «контекст» исходного местоположения, мне просто нужно захватить SynchronizationContext, сохранить его, а затем, когда я закончу свою работу, вызвать Post в этом контексте и передать делегат, который я хочу вызвать. Мне не нужно знать, что для Windows Forms я должен взять Control и использовать его BeginInvoke, или для WPF я должен взять Dispatcher и использовать его BeginInvoke, и т.п. Мне просто нужно взять текущий SynchronizationContext и использовать его позже. Чтобы достичь этого, SynchronizationContext предоставляет свойство Current, так что для достижения вышеупомянутой цели можно написать такой код:
public void DoWork(Action worker, Action completion)
{
SynchronizationContext sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
try { worker(); }
finally { sc.Post(_ => completion(), null); }
});
}
Платформа, которая хочет предоставить свой контекст в свойстве Current, использует метод SynchronizationContext.SetSynchronizationContext.

Источник: https://devblogs.microsoft.com/dotnet/configureawait-faq/
👍1
День триста двадцать третий. #AsyncAwaitFAQ
FAQ по async/await и ConfigureAwait
2. Что такое планировщик задач?
Планировщик задач (TaskScheduler) представляет собой объект, выполняющий низкоуровневую работу с очередями в потоках. Он гарантирует, что работа в задаче рано или поздно будет выполнена.
Когда задачи представляют из себя делегаты, которые могут быть поставлены в очередь и выполнены, они связываются с System.Threading.Tasks.TaskScheduler. Так же, как SynchronizationContext предоставляет виртуальный метод Post для постановки в очередь вызова делегата (с реализацией, позже вызывающей делегат с помощью типичных механизмов вызова делегата), TaskScheduler предоставляет абстрактный метод QueueTask (с реализацией, позже вызывающей эту задачу через метод ExecuteTask).
Класс TaskScheduler также служит точкой расширения для всей настраиваемой логики планирования. Планировщик по умолчанию, возвращаемый TaskScheduler.Default, является пулом потоков, но можно унаследовать от TaskScheduler и переопределить соответствующие методы для изменения того, когда и где вызывается Task. Например, в базовых библиотеках есть тип System.Threading.Tasks.ConcurrentExclusiveSchedulerPair. Экземпляр этого класса предоставляет два свойства типа TaskScheduler: ExclusiveScheduler и ConcurrentScheduler. Задачи, запланированные через ConcurrentScheduler, могут выполняться одновременно, но до определённого предела, назначенного ConcurrentExclusiveSchedulerPair при его создании. Но никакие задачи ConcurrentScheduler не будут выполняться, когда выполняется задача, запланированная для ExclusiveScheduler, в котором только одна задача может выполняться в определённый момент времени. Таким образом, поведение ExclusiveScheduler очень похоже на блокировку чтения/записи.
Как и SynchronizationContext, TaskScheduler также имеет свойство Current, которое возвращает «текущий» TaskScheduler. Однако, в отличие от SynchronizationContext, здесь нет способа установки текущего планировщика. Вместо этого текущий планировщик - тот, который связан с текущей выполняющейся задачей, и он предоставляется системе как часть процесса запуска задачи. Так, например, следующая программа выведет «True», так как лямбда-выражение в StartNew выполняется в ExclusiveScheduler объекта ConcurrentExclusiveSchedulerPair, и оно «увидит», что на этот планировщик установлен TaskScheduler.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. Все экземпляры Task, поставленные в очередь возвращённого планировщика, будут выполнены с помощью вызова метода Post в этом контексте.

Источник: https://devblogs.microsoft.com/dotnet/configureawait-faq/
День триста двадцать пятый. #AsyncAwaitFAQ
FAQ по async/await и ConfigureAwait
3. Как SynchronizationContext и TaskScheduler связаны с await?
Представьте кнопку в приложении. Нажав на кнопку, мы хотим загрузить текст с веб-сайта и установить его в качестве текста кнопки. Доступ к кнопке возможен только из UI-потока, которому она принадлежит, поэтому, когда мы успешно загрузили новый текст и хотим установить его как текст кнопки, мы должны сделать это из потока, которому принадлежит элемент управления (из UI-потока). Если мы этого не сделаем, мы получим исключение:
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.' (Вызывающий поток не может получить доступ к этому объекту, поскольку он принадлежит другому потоку).
Если бы мы писали это вручную, мы могли бы использовать SynchronizationContext, чтобы выполнить установку текста в исходном контексте, например через TaskScheduler:
private static readonly HttpClient сlient = new HttpClient();
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
сlient.GetStringAsync("http://example.com/gettext")
.ContinueWith(downloadTask => {
downloadBtn.Text = downloadTask.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());
}
Либо можно напрямую использовать SynchronizationContext:
private void downloadBtn_Click(object sender, RoutedEventArgs e)
{
SynchronizationContext sc = SynchronizationContext.Current;
сlient.GetStringAsync("http://example.com/gettext")
.ContinueWith(downloadTask => {
sc.Post(delegate {
downloadBtn.Text = downloadTask.Result;
}, null);
});
}
Однако оба этих подхода явно используют функции обратного вызова. Вместо этого мы можем написать код естественным образом с помощью async/await:
private async void downloadBtn_Click(object sender, RoutedEventArgs e)
{
string text = await сlient.GetStringAsync("http://example.com/gettext");
downloadBtn.Text = text;
}
Этот код «просто работает», успешно устанавливая текст в UI-потоке, потому что, как и в случае с вручную реализованными версиями выше, ожидание задачи по умолчанию запоминает текущий контекст (SynchronizationContext.Current) или текущий планировщик (TaskScheduler.Current).
Когда вы ожидаете чего-либо в C#, компилятор использует паттерн awaitable и просит «ожидаемый» (awaitable) объект (в данном случае Task) предоставить ему объект «ожидателя» (awaiter) – в данном случае TaskAwaiter<string>. Этот «ожидатель» отвечает за создание метода обратного вызова (часто называемого «продолжением»), который «вернёт выполнение в исходный код» после того, как задача завершит работу. И делает он это с захватом текущего контекста/планировщика. В упрощённом виде это выглядит так:
object scheduler = SynchronizationContext.Current;
if (scheduler is null && TaskScheduler.Current != TaskScheduler.Default)
{
scheduler = TaskScheduler.Current;
}
Другими словами, сначала проверяется, установлен ли SynchronizationContext, и, если нет, установлен ли «нестандартный» TaskScheduler. Если таким образом контекст/планировщик будет захвачен, он будет использован для выполнения метода обратного вызова соответственно в контексте или через планировщик. В противном случае чаще всего метод обратного вызова выполнится в том же контексте, что и ожидаемая задача.

Источник: https://devblogs.microsoft.com/dotnet/configureawait-faq/
День триста двадцать восьмой. #AsyncAwaitFAQ
FAQ по async/await и ConfigureAwait
4. Что делает ConfigureAwait(false)?
Метод ConfigureAwait, принимающий параметр continueOnCapturedContextпродолжитьВЗахваченномКонтексте»), возвращает структуру ConfiguredTaskAwaitable, которая содержит специальным образом настроенный «ожидатель» (awaiter). Поскольку await может использоваться с любым типом, соответствующим паттерну awaitable (не только с Task напрямую), возврат другого типа вместо задачи позволяет изменить логику захвата контекста/планировщика (см. предыдущий пост) на что-то вроде этого:
object scheduler = null;
if (continueOnCapturedContext)
{
// захват контекста/планировщика
}
Другими словами, получая значение false, объект не захватывает текущий контекст/планировщик, даже если они существуют.

Зачем использовать ConfigureAwait(false)?
Этот метод используется, чтобы избежать принудительного вызова метода обратного вызова в исходном контексте или планировщике. Это даёт несколько преимуществ:
1. Улучшает производительность. Требуется дополнительная работа, чтобы поставить в очередь метод обратного вызова вместо того, чтобы просто вызывать его. Кроме того, некоторые оптимизации времени выполнения, не могут быть использованы. Иногда даже проверка текущих контекста и планировщика (что включает в себя доступ к статическим потокам) могут добавить измеримые накладные расходы. Если код после await фактически не требует запуска в исходном контексте, использование ConfigureAwait(false) поможет избежать всех этих затрат.
2. Позволяет избежать взаимных блокировок. Взаимная блокировка чаще всего возникает при использовании кода, блокирующего текущий поток (при вызове .Wait(), .Result или .GetAwaiter().GetResult() на задаче, внутри которой используется await). Если в текущем контексте явно или неявно ограничено число параллельных операций, либо если код блокирует UI-поток, получается ситуация, когда контекст/поток заблокирован в ожидании завершения асинхронной задачи, а метод обратного вызова этой задачи ставится в очередь для вызова на заблокированном контексте/потоке и поэтому не может быть выполнен (взаимная блокировка). Такая ситуация может возникать, когда ресурсы ограничены любым способом и так или иначе оказываются заняты. Если задача использует ConfigureAwait(false), метод обратного вызова не ставится в очередь в исходный контекст, что позволяет избежать сценариев взаимоблокировки.

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

Источник: https://devblogs.microsoft.com/dotnet/configureawait-faq/