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 代码可能会为所有订阅者同时运行(当引发事件时,订阅者的三个异步事件处理程序被调用,每个都启动一个异步操作,该操作可能在不同线程上同时运行由任务调度程序)。还是我错了?

谢谢 !

4

2 回答 2

2

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

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

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

于 2012-11-30T03:02:42.237 回答
1

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

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

在这种情况下,不变性不仅可以帮助您轻松实现并发处理程序,而且可以带来更清晰的设计并提高可维护性,因为现在人们可以不正确地使用您的 API。

结论是:你应该实现你的方法线程安全,但你应该知道,如果你的事件将从非 UI 线程触发,来自事件处理程序的未处理异常将被吞没。

这是使用 async 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();
    }
}
于 2012-11-29T22:12:48.050 回答