异步事件处理程序和并发性

4
在 C# 控制台应用程序的上下文中,如果我创建一个循环用于异步接收消息,并为每个接收到的消息引发事件,例如:
while (true)
{
   var message = await ReceiveMessageAsync();
   ReceivedMessage(new ReceivedMessageEventArgs(message));
}

现在假设我有多个订阅者订阅了事件(为了例子,假设有3个订阅者),他们都使用异步事件处理程序,例如:
async void OnReceivedMessageAsync(object sender, ReceivedMessageEventArgs args)
{
   await TreatMessageAsync(args.Message);
}

消息对象是否应该以线程安全的方式编码?我认为是这样的,因为当事件触发时,不同订阅者的TreatMessageAsync代码可能会同时运行(调用三个订阅者的异步事件处理程序,每个处理程序都启动一个异步操作,任务调度程序可能会在不同的线程上并发运行)。或者我错了吗?

谢谢!


你在这里没有给我们足够的细节 - 没有表明TreatMessageAsync是什么,或者事件是什么。如果您能提供一个简短但完整的程序,那将使生活变得更加简单。 - Jon Skeet
2个回答

2

您应该以线程安全的方式编写代码。最简单的方法是使其不可变。

如果您有一个真正的事件,那么它的参数应该是不可变的。如果您正在为不是真正的事件(如命令实现)使用事件处理程序,则可能需要修改您的 API。

可以拥有并发的事件处理程序,因为每个处理程序将按顺序启动,但它们可以并发恢复


感谢您的清晰回答,很抱歉延迟了标记已回答的时间。经过一些思考,我意识到这不是正确的方法...我确实在为不是“真正事件”的事情使用事件处理程序。我相应地修改了我的原型。 - darkey

1

正如Stephen所建议的,在这种情况下实现线程安全的最简单方法是使用不可变的事件参数。

在大多数情况下,事件参数仅用于通知观察者,而没有来自可观察方到观察者(即从事件订阅者到事件所有者)的更改要求。在某些特殊情况下,例如实现责任链设计模式时,事件参数应该是可变的,但在所有其他情况下都不应该是可变的。

在这种情况下的不可变性不仅有助于您轻松实现并发处理程序,而且还会导致更清晰的设计,并提高可维护性,因为现在可以避免错误地使用您的API。

结论是:您应该实现方法的线程安全性,但您应该意识到,如果您的事件从非UI线程触发,则未处理的事件处理程序异常将被吞噬。

这里存在使用异步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();
    }
}

不完全正确。与任何异步 void 方法一样,处理程序将按顺序开始,并在它们 yield 时异步继续执行。void 返回类型意味着即使发送方想要等待所有处理程序,它也无法等待。从 http://msdn.microsoft.com/en-us/library/hh156513.aspx 的文档中可以看到:“调用异步方法的 void 返回值的调用者不能等待它,也不能捕获该方法抛出的异常。” - Panagiotis Kanavos
@PanagiotisKanavos所说的“void-returning method(无返回值方法)的调用者无法捕获该方法抛出的异常”仅适用于void方法第一个await之后的代码。在第一个await之前抛出的任何异常都可以被调用者捕获。 - Francois Nel
1
@FrancoisNel:在最初的Async CTP期间,这是真实的。在第一个CTP刷新中,[行为被更改为更加一致](http://blogs.msdn.com/b/lucian/archive/2011/04/15/async-ctp-refresh-design-changes.aspx)。MSDN文档正确描述了Visual Studio 2012中的“异步”行为。 - Stephen Cleary
@StephenCleary:请查看我的代码示例以获取更多详细信息。尝试在控制台应用程序中使用这种方法会导致DomainUnhandled异常。顺便说一下,线程池“SynchronizationContext”等于null。 - Sergey Teplyakov
@StephenCleary:同意。我的错,实际上我期望的是任务观察到异常而不是域未处理的异常。但无论如何,开发人员都应该注意这种行为。 - Sergey Teplyakov
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接