Асинхронные обработчики событий и параллелизм

В контексте консольного приложения С#, если я создаю цикл, используемый для асинхронного получения сообщения, который вызывает событие для каждого полученного сообщения, например:

while (true)
{
   var message = await ReceiveMessageAsync();
   ReceivedMessage(new ReceivedMessageEventArgs(message));
}

Теперь, если у меня есть несколько подписчиков на событие (скажем, 3 подписчика для примера), все они используют асинхронный обработчик событий, например:

async void OnReceivedMessageAsync(object sender, ReceivedMessageEventArgs args)
{
   await TreatMessageAsync(args.Message);
}

Должен ли объект сообщения быть закодирован потокобезопасным способом? Я так думаю, поскольку код TreatMessageAsync из разных обработчиков событий может выполняться одновременно для всех подписчиков (когда возникает событие, вызываются три обработчика асинхронных событий подписчиков, каждый из которых запускает асинхронную операцию, которая потенциально может выполняться одновременно в разных потоках). планировщиком задач). Или я ошибаюсь?

Спасибо !


person darkey    schedule 29.11.2012    source источник
comment
Вы не предоставили нам достаточно подробностей — нет никаких указаний на то, что такое TreatMessageAsync или что это за событие. Если бы вы могли дать короткую, но полную программу, это сделало бы жизнь намного проще.   -  person Jon Skeet    schedule 30.11.2012


Ответы (2)


Вы должны кодировать его потокобезопасным способом. Самый простой способ сделать это — сделать его неизменным.

Если у вас есть истинное событие, его аргументы должны быть неизменяемыми. Если вы используете обработчик событий для чего-то, что не является настоящим событием (например, команда или реализация), вы можете изменить свой API.

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

person Stephen Cleary    schedule 30.11.2012
comment
Спасибо за четкий ответ и извините за задержку с пометкой ответа. После некоторого размышления я понял, что это неправильный путь. Я действительно использовал обработчик событий для чего-то, что не было истинным событием. Я соответствующим образом модифицировал свой прототип. - person darkey; 05.12.2012

Как предположил Стивен, самый простой способ добиться потокобезопасности в этом случае — использовать неизменяемые аргументы событий.

В большинстве случаев даже аргументы используются только для уведомления наблюдателей без каких-либо требований для изменений от наблюдаемой стороны к наблюдателю (т.е. от подписчика события к владельцу события). В некоторых особых случаях, таких как реализация шаблона проектирования цепочки ответственности, аргументы событий должны быть изменяемыми, но во всех других случаях они не должны.

Неизменяемость в этом случае помогает вам не только легко реализовывать параллельные обработчики, но и приводит к более четкому дизайну и повышает удобство сопровождения, поскольку теперь у вас есть возможность неправильно использовать ваш API.

Вывод: вы должны реализовать потокобезопасность своих методов, но вы должны знать, что необработанное исключение из обработчика событий будет проглочено, если ваше событие будет запущено из потока, отличного от пользовательского интерфейса.

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

internal class Program
{
    static Task Boo()
    {
        return Task.Run(() =>
                        {
                            throw new Exception("111");
                        });
    }

    private static async void Foo()
    {
        await Boo();
    }

    static void Main(string[] args)
    {
        // Application will blow with DomainUnhandled excpeption!
        try
        {
            Foo();
        }
        catch (Exception e)
        {
            // Will not catch it here!
            Console.WriteLine(e);
        }

        Console.ReadLine();
    }
}
person Sergey Teplyakov    schedule 29.11.2012
comment
Не совсем. Как и в случае любого асинхронного метода void, обработчики будут запускаться последовательно и продолжаться асинхронно, как только они завершатся. Тип возвращаемого значения void означает, что отправитель не сможет ожидать всех обработчиков, даже если захочет. Из документа по адресу msdn.microsoft.com/en-us/library/hh156513. aspx Вызывающий объект асинхронного метода, возвращающего void, не может ожидать его и не может перехватывать исключения, которые создает метод. - person Panagiotis Kanavos; 30.11.2012
comment
@PanagiotisKanavos Вызывающий метод, возвращающий void ... не может перехватывать исключения, которые генерирует метод, верно только для кода после первого ожидания метода void. Любые исключения, созданные до первого ожидания, могут быть перехвачены вызывающей стороной. - person Francois Nel; 30.11.2012
comment
Это цитата из документации. Это уже объяснено по предоставленной ссылке (сразу после первого примера кода) - person Panagiotis Kanavos; 30.11.2012
comment
@FrancoisNel: Это было правдой во время оригинальной асинхронной CTP. При первом обновлении CTP этот поведение было изменено, чтобы быть более последовательным. Документация MSDN правильно описывает поведение async в Visual Studio 2012. - person Stephen Cleary; 30.11.2012
comment
@SergeyTeplyakov: исключения из async void обработчиков событий не проглатываются; их просто невозможно поймать. Они проходят прямо к SynchronizationContext и поднимаются там. - person Stephen Cleary; 03.12.2012
comment
@StephenCleary: а что, если у нас нет контекста синхронизации? - person Sergey Teplyakov; 03.12.2012
comment
@SergeyTeplyakov: тогда они поднимаются по умолчанию (пул потоков) SynchronizationContext. - person Stephen Cleary; 03.12.2012
comment
@StephenCleary: см. мой пример кода для более подробной информации. Попытка использовать такой метод в консольном приложении приведет к исключению DomainUnhandled. Кстати, пул потоков SynchronizationContext равен нулю. - person Sergey Teplyakov; 03.12.2012
comment
DomainUnhandled противоположно проглоченному. Проглотил - значит проигнорировал. - person Stephen Cleary; 04.12.2012
comment
@StephenCleary: Согласен. Плохо, на самом деле я ожидал исключения задачи onobserved вместо необработанного исключения домена. Но в любом случае разработчик должен знать о таком поведении. - person Sergey Teplyakov; 04.12.2012
comment
спасибо за комментарии, очень интересно! +1 за ваш ответ Сергей. - person darkey; 05.12.2012