异步事件处理程序和并发

时间:2012-11-29 20:43:18

标签: c# task-parallel-library async-await

在C#控制台应用程序的上下文中,如果我创建一个用于异步接收消息的循环,它会为收到的每条消息引发一个事件,例如:

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

现在,如果我有多个订阅者(为了示例,让我们说3个订阅者),所有这些订阅者都使用异步事件处理程序,例如:

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

消息对象应该以线程安全的方式编码吗?我是这么认为的,因为来自不同事件处理程序的TreatMessageAsync代码可以为所有订阅者运行得很清楚(当引发事件时,调用订阅者的三个异步事件处理程序,每个都启动异步操作,这可能可以在不同的线程上运行由任务调度程序)。或者我错了吗?

谢谢!

2 个答案:

答案 0 :(得分:2)

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

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

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

答案 1 :(得分:1)

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

在大多数情况下,甚至args仅用于通知观察者,而不需要从可观察侧到观察者(即从事件订阅者到事件所有者)的更改。在某些特殊情况下,如实施责任链设计模式事件args应该是可变的,但在所有其他情况下它们不应该是。

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

结论是:你应该实现你的方法thread-safety,但你应该知道,如果你的事件将从非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();
    }
}