【问题标题】:Async event handlers and concurency异步事件处理程序和并发
【发布时间】:2012-11-29 20:43:17
【问题描述】:

在 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 是什么,或者事件是什么。如果你能给出一个简短但完整的程序,那会让生活变得简单很多。

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


【解决方案1】:

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

如果你有一个真正的事件,那么它的参数应该是不可变的。如果您使用的事件处理程序不是真正的 event(例如 commandimplementation),那么您可能需要修改您的API。

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

【讨论】:

  • 感谢您的明确回答,并抱歉延迟将其标记为已回答。经过一番反思,我意识到这不是正确的方法......我确实在使用事件处理程序来处理不是“真实事件”的事情。我相应地修改了我的原型。
【解决方案2】:

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

在大多数情况下,甚至 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();
    }
}

【讨论】:

  • 不完全。与任何 async void 方法一样,处理程序将按顺序启动,并在它们产生后立即异步继续。 void 返回类型意味着发送者将无法等待所有处理程序,即使它想要。来自msdn.microsoft.com/en-us/library/hh156513.aspx 的文档“返回 void 的异步方法的调用者无法等待它,也无法捕获该方法抛出的异常。”
  • @PanagiotisKanavos “返回 void 的方法的调用者......无法捕获该方法抛出的异常”仅适用于 void 方法的第一个等待之后的代码。在第一次等待之前抛出的任何异常都可以被调用者捕获。
  • 这是来自文档的引用。它已经在提供的链接中进行了解释(就在第一个代码示例之后)
  • @FrancoisNel:在最初的 Async CTP 期间确实如此。在第一次 CTP 刷新中,behavior was changed to be more consistent。 MSDN 文档正确描述了 Visual Studio 2012 中的 async 行为。
  • @SergeyTeplyakov:来自async void 事件处理程序的异常不会被吞没;他们就是不能被抓住。它们直接传递给SynchronizationContext 并在那里提出。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-06
相关资源
最近更新 更多