Так как оригинальная статья довольно объемная, я взял на себя смелость разбить ее на несколько независимых частей, более легких для перевода и восприятия.
Disclaimer: Я не являюсь профессиональным переводчиком, перевод подготовлен скорее для себя и коллег. Я буду благодарен за любые исправления и помощь в переводе, статья очень интересная давайте сделаем её доступной на русском языке.
-
Появление Tasks (Асинхронная модель на основе задач (TAP)
...и ValueTasks
-
Итераторы C# в помощь
-
Async/await: Внутреннее устройство
Преобразования компилятора
SynchronizationContext и ConfigureAwait
Поля в State Machine
-
Заключение
Асинхронная модель на основе событий (EAP)
В NET Framework 2.0 было представлено несколько API, реализующих другой паттерн для обработки асинхронных операций, предназначенный в первую очередь для выполнения их в контексте клиентских приложений. Этот Event-based Asynchronous Pattern, или EAP, также состоял из пары членов (как минимум, возможно, больше), на этот раз метода для инициирования асинхронной операции и события для прослушивания ее завершения. Таким образом, наш предыдущий пример DoStuff мог бы быть представлен в виде набора членов, подобных этому:
class Handler
{
public int DoStuff(string arg);
public void DoStuffAsync(string arg, object? userToken);
public event DoStuffEventHandler? DoStuffCompleted;
}
public delegate void DoStuffEventHandler(object sender, DoStuffEventArgs e);
public class DoStuffEventArgs : AsyncCompletedEventArgs
{
public DoStuffEventArgs(int result, Exception? error, bool canceled, object? userToken) :
base(error, canceled, usertoken) => Result = result;
public int Result { get; }
}
Вы регистрируете свою работу по продолжению с событием DoStuffCompleted, а затем вызываете метод DoStuffAsync; он инициирует операцию, и по ее завершении событие DoStuffCompleted будет асинхронно поднято вызывающей стороной. После этого обработчик может продолжить свою работу, вероятно, проверяя, что предоставленный userToken соответствует ожидаемому, что позволяет подключить к событию несколько обработчиков одновременно.
Этот паттерн немного упростил некоторые случаи использования, но при этом значительно усложнил другие (а учитывая предыдущий пример APM CopyStreamToStream, это о чем-то говорит). Он не получил широкого распространения, а появился и исчез фактически в одном выпуске .NET Framework, хотя и оставил после себя API, добавленные во время его существования, такие как Ping.SendAsync/Ping.PingCompleted:
public class Ping : Component
{
public void SendAsync(string hostNameOrAddress, object? userToken);
public event PingCompletedEventHandler? PingCompleted;
...
}
Тем не менее, он добавил одно заметное достижение, которое не было учтено в модели APM, и которое сохранилось в моделях, которые мы используем сегодня: SynchronizationContext.
SynchronizationContext был также представлен в .NET Framework 2.0 в качестве абстракции для общего планировщика. В частности, наиболее используемым методом SynchronizationContext является Post, который ставит рабочий элемент в очередь к любому планировщику, представленному этим контекстом.
Базовая реализация SynchronizationContext, например, просто представляет ThreadPool, и поэтому базовая реализация SynchronizationContext.Post просто делегирует ThreadPool.QueueUserWorkItem, который используется, чтобы попросить ThreadPool вызвать предоставленный обратный вызов с соответствующим состоянием на одном из потоков пула. Однако суть SynchronizationContext заключается не только в поддержке произвольных планировщиков, а скорее в поддержке планирования таким образом, чтобы оно работало в соответствии с потребностями различных моделей приложений.
Рассмотрим такую структуру пользовательского интерфейса, как Windows Forms. Как и в большинстве фреймворков пользовательского интерфейса Windows, элементы управления связаны с определенным потоком, и этот поток запускает цикл обработки сообщений, который выполняет работу, способную взаимодействовать с этими элементами управления: только этот поток должен пытаться манипулировать этими элементами управления, а любой другой поток, который хочет взаимодействовать с элементами управления, должен сделать это, отправив сообщение, которое будет потреблено циклом обработки сообщений потока пользовательского интерфейса. Windows Forms упрощает эту задачу с помощью таких методов, как Control.BeginInvoke, который ставит в очередь предоставленный делегат и аргументы для выполнения любым потоком, связанным с данным элементом управления. Таким образом, вы можете написать код, подобный этому:
private void button1_Click(object sender, EventArgs e)
{
ThreadPool.QueueUserWorkItem(_ =>
{
string message = ComputeMessage();
button1.BeginInvoke(() =>
{
button1.Text = message;
});
});
}
Это позволит разгрузить работу ComputeMessage() для выполнения в потоке ThreadPool (чтобы пользовательский интерфейс оставался отзывчивым во время обработки), а затем, когда эта работа завершится, передать делегат обратно в поток, связанный с button1, для обновления метки button1. Достаточно просто. В WPF есть нечто подобное, только с типом Dispatcher:
private void button1_Click(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(_ =>
{
string message = ComputeMessage();
button1.Dispatcher.InvokeAsync(() =>
{
button1.Content = message;
});
});
}
И в .NET MAUI есть нечто подобное. Но что если я хочу поместить эту логику во вспомогательный метод? Например:
// Call ComputeMessage and then invoke the update action to update controls.
internal static void ComputeMessageAndInvokeUpdate(Action<string> update) { ... }
Затем я могу использовать это следующим образом:
private void button1_Click(object sender, EventArgs e)
{
ComputeMessageAndInvokeUpdate(message => button1.Text = message);
}
но как можно реализовать ComputeMessageAndInvokeUpdate таким образом, чтобы он мог работать в любом из этих приложений? Нужно ли его жестко кодировать, чтобы он знал о каждом возможном фреймворке пользовательского интерфейса? Вот где SynchronizationContext нам поможет. Мы можем реализовать метод следующим образом:
internal static void ComputeMessageAndInvokeUpdate(Action<string> update)
{
SynchronizationContext? sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
string message = ComputeMessage();
if (sc is not null)
{
sc.Post(_ => update(message), null);
}
else
{
update(message);
}
});
}
Это использует SynchronizationContext как абстракцию, чтобы нацелить любой «планировщик», который должен быть использован, чтобы вернуться к необходимой среде для взаимодействия с пользовательским интерфейсом. Затем каждая модель приложения обеспечивает публикацию в качестве SynchronizationContext.Current производного от SynchronizationContext типа, который делает "правильную вещь". Например, Windows Forms имеет следующее:
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable
{
public override void Post(SendOrPostCallback d, object? state) =>
_controlToSendTo?.BeginInvoke(d, new object?[] { state });
...
}
и в WPF это есть:
public sealed class DispatcherSynchronizationContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, Object state) =>
_dispatcher.BeginInvoke(_priority, d, state);
...
}
Раньше в ASP.NET был один, который не заботился о том, в каком потоке выполняется работа, а скорее о том, чтобы работа, связанная с данным запросом, была сериализована таким образом, чтобы несколько потоков не могли одновременно обращаться к данному HttpContext:
internal sealed class AspNetSynchronizationContext : AspNetSynchronizationContextBase
{
public override void Post(SendOrPostCallback callback, Object state) =>
_state.Helper.QueueAsynchronous(() => callback(state));
...
}
Это также не ограничивается такими основными моделями приложений. Например, xunit - это популярный фреймворк модульного тестирования, который используется в основных репозиториях .NET для модульного тестирования, и он также использует несколько пользовательских SynchronizationContexts. Вы можете, например, разрешить тестам выполняться параллельно, но ограничить количество тестов, которые могут выполняться одновременно. Как это можно сделать? С помощью SynchronizationContext:
public class MaxConcurrencySyncContext : SynchronizationContext, IDisposable
{
public override void Post(SendOrPostCallback d, object? state)
{
var context = ExecutionContext.Capture();
workQueue.Enqueue((d, state, context));
workReady.Set();
}
}
Метод Post контекста MaxConcurrencySyncContext просто ставит работу в свою собственную внутреннюю очередь, которую затем обрабатывает на своих собственных рабочих потоках, где он контролирует их количество в зависимости от желаемого максимального параллелизма. Вы поняли идею.
Как это связано с Event-based Asynchronous Pattern? EAP и SynchronizationContext были введены одновременно, и EAP диктовал, что события завершения должны быть поставлены в очередь к тому SynchronizationContext, который был текущим, когда была инициирована асинхронная операция.
Чтобы немного упростить эту задачу (и, вероятно, не настолько, чтобы оправдать дополнительную сложность), в System.ComponentModel также были введены некоторые вспомогательные типы, в частности AsyncOperation и AsyncOperationManager. Первый был просто кортежем, который обертывал предоставленный пользователем объект состояния и захваченный SynchronizationContext, а второй просто служил простой фабрикой для выполнения захвата и создания экземпляра AsyncOperation. Затем реализации EAP использовали их, например, Ping.SendAsync вызывал AsyncOperationManager.CreateOperation для захвата SynchronizationContext, а затем, когда операция завершалась, вызывался метод PostOperationCompleted AsyncOperation для вызова метода Post сохраненного SynchronizationContext.
SynchronizationContext предоставляет еще несколько «финтифлюшек», о которых стоит упомянуть, поскольку они еще не раз появятся. В частности, он раскрывает методы OperationStarted и OperationCompleted. Базовая реализация этих виртуальных методов пуста, ничего не делает, но производная реализация может переопределить их, чтобы знать об операциях в ходе выполнения. Это означает, что реализации EAP будут также вызывать эти OperationStarted/OperationCompleted в начале и конце каждой операции, чтобы информировать любой присутствующий SynchronizationContext и позволить ему отслеживать работу. Это особенно актуально для паттерна EAP, поскольку методы, инициирующие асинхронные операции, возвращают пустоту: вы не получаете ничего, что позволило бы вам отслеживать работу по отдельности. К этому мы еще вернемся.
Итак, нам нужно было что-то лучшее, чем шаблон APM, а EAP, который появился следом, ввел некоторые новые вещи, но не решил основные проблемы, с которыми мы столкнулись. Нам все еще нужно было что-то лучшее.
Комментарии (7)
hartmeyerorum888
11.04.2023 21:57+1Автор, у вас дублируются параграфы: "Как это связано с Event-based Asynchronous Pattern..."
AlekseiNesterov3211
11.04.2023 21:57-4Async - это асинхронный подход к программированию, который позволяет выполнять задачи параллельно и не блокировать основной поток выполнения.
Async работает следующим образом:
1. Создается асинхронная функция, которая обычно помечается ключевым словом async.
2. Внутри этой функции могут быть выполнены асинхронные операции, такие как чтение или запись файлов, отправка запросов на сервер и т.д.
3. Вместо того, чтобы ждать завершения каждой операции, асинхронная функция продолжает работу и возвращает объект Promise.
4. Когда все асинхронные операции завершены, Promise переходит в состояние resolved и возвращает результат выполнения функции.
5. Если произошла ошибка во время выполнения асинхронных операций, Promise переходит в состояние rejected и возвращает объект ошибки.
6. Для обработки результата асинхронной функции используются методы then и catch объекта Promise.
Async позволяет увеличить производительность приложения, поскольку выполнение задач происходит параллельно, а также улучшает пользовательский опыт, поскольку приложение не блокируется во время выполнения длительных операций.
mvv-rus
11.04.2023 21:57чтобы знать об операциях в полете
А можно это на русский перевести? Чтобы читателям не приходилось переводить сначала дословно на английский, а потом — по смыслу на русский.
PS Это не единственный огрех перевода, но самый выразительный IMHOnkz-soft Автор
11.04.2023 21:57Большое спасибо!
Могу я вас попросить написать в личку по проблемам перевода и мы попробуем вместе сделать перевод лучше.
OlegTar
В свое время изучал как работает async await, читал Рихтера, Албахари, статьи, смотрел видосы, экспериментировал.
И тоже узнал, что async await использует под капотом synchronizationcontext, который в свою очередь посылает код на выполнение основному потоку, и в зависимости от фреймворка (wpf, winforms, asp.net) делает это по-разному).
Но тут в статье даже какие-то внутренности показаны для asp.net