Каковы текущие рекомендации по использованию API-интерфейсов async/await для замены событий .NET?

Рассмотрим случай библиотеки, предназначенной для использования с другими проектами, которыми могут быть консольные приложения, другие библиотеки классов или приложения с пользовательским интерфейсом. Эта библиотека позволяет пользователю отправлять команды и получать ответы от удаленной конечной точки, но также может получать сообщения о событиях от удаленной конечной точки. Допустимым вариантом использования является то, что пользователь может отправить команду в ответ на удаленное событие. Транспортный механизм использует методы 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?

Вы смотрели на Reactive Framework от Microsoft?

Enigmativity 06.06.2024 03:41

Я также не уверен, что вы считаете плохим? События вызываются методами async? В этом нет ничего плохого. Я упускаю суть вашего поста.

Enigmativity 06.06.2024 06:27
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
124
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

События .NET не работают с async/await

Истинный

Никогда не используйте async void

Верно, за исключением случаев, когда вас вынуждают к этому события в устаревших технологиях (Winforms, ваш код и т. д.). См. выше.

Я полностью понимаю, почему это неоптимально

Чтобы быть абсолютно ясным, ваш пример кода задачи очень и очень плох. По многим другим причинам, чем те, которые вы перечислили. И на самом деле, вы даже не упомянули то, что хотели улучшить, события. У вас тот же API событий, что и в версии без задач, только с худшим кодом, который больше даже не компилируется.

Два момента, которые помогут вам сделать API более чистым:

  1. Тот факт, что события «не работают» с задачами (читай: они вытекают из исходного вызова), похоже, не применим в вашем случае. Вы просто запускаете событие с копией данных (результат сериализации), поэтому даже если вы потеряете кадр стека, ваш параметр не получит GC. Так что с тобой все в порядке.

  2. Современные события, связанные с задачами C#, инкапсулируются в реализациях MVVM ICommand, например, ReactiveCommand ReactiveUI или RelayCommand сообщества MVVM. Оба (и другие) имеют полную поддержку функций асинхронной поддержки и правильно обрабатывают, например, отключение команды во время ее выполнения.

Да, включенный код плохой. Мне было неясно, что это так, и это представлено просто в целях обсуждения. Моя вина. Я отредактировал, чтобы добавить несколько заявлений об отказе от ответственности по этому поводу.

JimEvans 05.06.2024 23:37
Ответ принят как подходящий

События .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, вероятно, ближе к тому, что ожидают пользователи. Я уже склонялся к этому направлению, так что я поиграю с этим. Большое спасибо за понимание и идеи.

JimEvans 06.06.2024 05:26

Другие вопросы по теме