Рассмотрим случай библиотеки, предназначенной для использования с другими проектами, которыми могут быть консольные приложения, другие библиотеки классов или приложения с пользовательским интерфейсом. Эта библиотека позволяет пользователю отправлять команды и получать ответы от удаленной конечной точки, но также может получать сообщения о событиях от удаленной конечной точки. Допустимым вариантом использования является то, что пользователь может отправить команду в ответ на удаленное событие. Транспортный механизм использует методы async/await для передачи данных на удаленный конец и обратно (например, с помощью WebSocket). В «классическом» .NET-приложении я мог бы разработать следующий API (Важное примечание: представленный здесь код не является каноническим, вероятно, не будет компилироваться «как есть» и написан только для целей обсуждения):
private Command currentCommand;
public event EventHandler<EventReceivedEventArgs>? EventReceived;
public CommandResponse ExecuteCommand(CommandParameters parameters)
{
currentCommand = transport.SendData(parameters.Serialize());
return currentCommand.WaitForCommandResponse();
}
protected void OnEventReceived(EventData eventData)
{
if (this.EventReceived is not null)
{
this.EventReceived(this, new EventReceivedEventArgs(eventData));
}
}
private void DataReceiver()
{
// Example only. In a real app, there would be some terminating
// condition here.
while (true)
{
// For discussion purposes, assume this an API that returns data
// from the remote end. How it works and how it is parsed can be
// considered an implementation detail.
byte[] receivedData = transport.ReceiveData();
ParsedData parsed = Parse(receivedData);
if (parsed is CommandResponse)
{
this.currentCommand.SetResponse(parsed);
}
if (parsed is EventData)
{
this.OnEventReceived(parsed);
}
}
}
Однако, учитывая, что я неоднократно слышал (и сегодня несколько многозначительно): «События .NET не работают с async/await», «Никогда не используйте async void» и «Не смешивайте синхронные и асинхронный код». Итак, учитывая форму async/await API, она выглядит так:
private Command currentCommand;
// This, right here, I'm told, is bad, bad, bad. Totally suspect.
// Poor API design choice. Don't do it. This API, and it's caller
// below are presented merely as placeholders for discussion.
public event EventHandler<EventReceivedEventArgs>? EventReceived;
public async Task<CommandResponse> ExecuteCommandAsync(CommandParameters parameters)
{
await transport.SendCommandAsync(parameters.Serialize());
return await transport.WaitForCommandResponse();
}
protected void OnEventReceived(EventData eventData)
{
if (this.EventReceived is not null)
{
this.EventReceived(this, new EventReceivedEventArgs(eventData));
}
}
private async Task DataReceiver()
{
while (true)
{
await byte[] receivedData = transport.ReceiveData();
ParsedData parsed = Parse(receivedData);
if (parsed is CommandResponse)
{
this.currentCommand.SetResponse(parsed);
}
if (parsed is EventData)
{
this.OnEventReceived(parsed);
}
}
return Task.CompletedTask;
}
Я полностью понимаю, почему это неоптимально. Обработчик событий потребителя заблокирует производящий метод. Он может зависнуть или работать очень долго. Может бросить. Существует множество вещей, которые могут быть проблематичными с этим стилем API.
Но вот оперативный вопрос: какова альтернатива? Что рекомендует сообщество разработчиков .NET в качестве способа предоставления пользователю таких возможностей вместо использования событий .NET?
Я также не уверен, что вы считаете плохим? События вызываются методами async? В этом нет ничего плохого. Я упускаю суть вашего поста.





События .NET не работают с async/await
Истинный
Никогда не используйте async void
Верно, за исключением случаев, когда вас вынуждают к этому события в устаревших технологиях (Winforms, ваш код и т. д.). См. выше.
Я полностью понимаю, почему это неоптимально
Чтобы быть абсолютно ясным, ваш пример кода задачи очень и очень плох. По многим другим причинам, чем те, которые вы перечислили. И на самом деле, вы даже не упомянули то, что хотели улучшить, события. У вас тот же API событий, что и в версии без задач, только с худшим кодом, который больше даже не компилируется.
Два момента, которые помогут вам сделать API более чистым:
Тот факт, что события «не работают» с задачами (читай: они вытекают из исходного вызова), похоже, не применим в вашем случае. Вы просто запускаете событие с копией данных (результат сериализации), поэтому даже если вы потеряете кадр стека, ваш параметр не получит GC. Так что с тобой все в порядке.
Современные события, связанные с задачами C#, инкапсулируются в реализациях MVVM ICommand, например, ReactiveCommand ReactiveUI или RelayCommand сообщества MVVM. Оба (и другие) имеют полную поддержку функций асинхронной поддержки и правильно обрабатывают, например, отключение команды во время ее выполнения.
Да, включенный код плохой. Мне было неясно, что это так, и это представлено просто в целях обсуждения. Моя вина. Я отредактировал, чтобы добавить несколько заявлений об отказе от ответственности по этому поводу.
События .NET не работают с async/await
Они не подходят по природе, нет. Большинство событий в .NET имеют void в качестве типа возвращаемого значения, а естественным типом возвращаемого значения для асинхронного метода без возвращаемого значения является Task, а не void.
Тем не менее, существуют различные способы заставить асинхронные события работать. Вы можете определить делегата с типом возвращаемого значения Task и объявить событие с этим типом делегата. Или вы можете использовать отсрочку. Или async void. Каждый из них может быть подходящим решением для использования async/await с обработчиками событий в зависимости от вашего варианта использования.
Никогда не используйте async void
Обычно я даю правило: «избегайте async void». Это не жесткое правило. Фактически, в частности, для событий пользовательского интерфейса async void обычно является тем, что вы хотите сделать.
Не смешивайте синхронный и асинхронный код
Определенно действительный. Здесь нет аргументов.
Какая альтернатива? Что рекомендует сообщество разработчиков .NET в качестве способа предоставления пользователю таких возможностей вместо использования событий .NET?
Во-первых, я рекомендую решить, хотите ли вы использовать шаблон производитель/потребитель, шаблон Наблюдатель или шаблон Стратегия. При написании синхронного кода Observer и Strategy часто путают, и для реализации любого из них обычно используются события. Технически события должны использоваться для Observer, а что-то еще (например, интерфейсы или переопределение производных методов) — для Strategy. И производитель/потребитель обычно использует потокобезопасную очередь между производителем(ами) и потребителем(ями).
Если то, что у вас есть, на самом деле является шаблоном стратегии, полностью отмените событие и замените его интерфейсом. Интерфейсы могут легко определять асинхронные методы. Но если у вас есть наблюдатель или производитель/потребитель, читайте дальше.
В ситуации Observer вы можете думать о своей библиотеке как о производителе событий, у которых могут быть слушатели. С этой точки зрения вы имеете дело с потоком событий или «потоком событий». (Обратите внимание, что использование здесь существительного «событие» не имеет ничего общего с event в C#; «событие» в этом абзаце относится к каждому поступающему сообщению данных).
Rx (Reactive Extensions) — это буквальная интерпретация шаблона Observer. С теоретической точки зрения это фантастическая замена C# event. К сожалению, кривая обучения значительна, поэтому на самом деле она не получила поддержки в сообществе .NET. У Rx также есть некоторые трудности с обработчиками async: у него много тех же проблем, что и у прямого void-возврата event. В общем, сейчас я обычно не рекомендую это делать.
Существуют также подходы event-с-необычным делегированием: как Task-возвращение, так и отсрочка. Я расскажу об этом более подробно в блоге , если вам интересно.
Но я предполагаю, что вам действительно нужен шаблон производитель/потребитель. Производитель/потребитель можно отличить от Observer тем, что элементы потребляются только один раз в производителе/потребителе, тогда как в Observer один и тот же элемент «транслируется» всем слушателям. Для шаблона производитель/потребитель лучшим подходом будут каналы.
System.Threading.Channels предоставляет каналы, которые можно рассматривать как асинхронную очередь производителя/потребителя. Ваш код все еще создает поток событий; они попадут в очередь (рекомендую использовать ограниченный канал). Затем вы можете предоставить потребителям сторону чтения этой очереди как IAsyncEnumerable<T>, которую ваши потребители смогут более естественно использовать, используя await foreach. С этим типом API «вытягивания» (в отличие от API «push» уведомлений, используемого событиями и Rx в шаблоне Observer) потребителям легче иметь дело.
Примечание: если ваши команды и их ответы могут быть сопоставлены с каким-либо идентификатором (что обычно имеет место в протоколах такого типа), рассмотрите возможность использования (параллельного) словаря активных команд, а не просто одного currentCommand. Это позволит вашим потребителям иметь несколько команд в полете.
У меня есть (очень длинный — извините!) серия видео о разработке асинхронного TCP/IP-клиента/сервера . Большая часть этого (TCP/IP и программирование сокетов) не будет иметь для вас никакого значения (поскольку вы используете WebSockets), но я показываю, как использовать каналы в качестве потоков сообщений, а также словарь параллельных активных действий. -командная техника.
Это именно тот ответ, который я ищу. Вы уже ожидали одну из вещей, которые я реализовал, прежде чем публиковать здесь (ConcurrentDictionary команд в полете). Да, для фактического распространения событий мне нужен производитель-потребитель, и я уже использую для этого Channel. Что касается того, как пользователи будут уведомляться о событиях, похоже, что шаблон Observer, вероятно, ближе к тому, что ожидают пользователи. Я уже склонялся к этому направлению, так что я поиграю с этим. Большое спасибо за понимание и идеи.
Вы смотрели на Reactive Framework от Microsoft?