【问题标题】:Marshalling exceptions across threads (without a SynchronizationContext)跨线程编组异常(没有 SynchronizationContext)
【发布时间】:2020-01-17 07:37:28
【问题描述】:

我有一个同步调用方法的事件处理程序。

void OnSomeEventHappened(int eventInfo)
{
    MethodDoingSomething(eventInfo);
}

我想更改此事件处理程序,以便方法调用(对 MethodDoingSomething)变为异步。

问题是 MethodDoingSomething 可能会抛出异常,重要的是在新的异步行为中,这些异常会继续从运行事件处理程序的线程中抛出(即,用于接收异常的原始线程)同步情况)。

我还需要按照它们到达的顺序执行调用。

我的做法是使用阻塞集合来实现一个FIFO任务队列。

void OnSomeEventHappened(int eventInfo)
{
    _blockingCollectionFifoQueue.Add(eventInfo);
}

并且有一个调用原始方法的阻塞集合的消费者:

void RunConsumer()
{
    foreach (var elem in _blockingCollectionFifoQueue.GetConsumingEnumerable())
    {
        MethodDoingSomething(elem);
    }
}

在那种情况下,我如何获取可能从 MethodDoingSomething 引发的异常,并在主线程中重新引发它们,就像之前代码同步时一样。

【问题讨论】:

    标签: c# multithreading asynchronous exception blockingcollection


    【解决方案1】:

    没有办法像在同步场景中那样在事件处理程序中重新抛出异常,因为 - 因为我们现在是异步的 - 我们无法确定在我们想要执行 OnSomeEventHappened 的线程上运行哪个方法扔。

    但是,如果有任何异常,您可以在该线程的SynchronizationContext 上发布异常。

    如果您的 OnSomeEventHappened 事件处理程序在 WPF 或 WinForms 应用程序的 UI 线程上运行,则您可以使用 SychronizationContext

    如果它是随机或线程池线程 - 这意味着该线程上没有 SynchronizationContext - 您将必须编写自己的实现(提示:不要,它是很难正确实施)。要在不同的线程上触发某些事情,您需要某种同步或消息传递系统。

    如果你想一直在同一个线程上重新抛出异常,你可以存储目标线程的同步上下文并使用这个引用:

    // Make sure this code is executed on the thread that will receive the exceptions.
    // Note that 'Current' will be null for thread pool threads
    this.exceptionsSyncContext = SynchronizationContext.Current;
    

    然后,您的阻塞收集处理可能如下所示:

    try
    {
        MethodDoingSomething(elem);
    }
    catch (Exception ex)
    {
        // Capture the exception into an ExceptionDispatchInfo so that its 
        // stack trace and Watson bucket info will be preserved
        var edi = ExceptionDispatchInfo.Capture(ex);
        this.exceptionsSyncContext.Post(state => ((ExceptionDispatchInfo)state).Throw(), edi);
    }
    

    如果您想在不同的线程上重新抛出异常,只需将相应的 SynchronizationContext 引用与您的 eventInfo 一起存储在同一集合中。例如。为此使用一个元组。

    【讨论】:

    • 谢谢。看来我没有 SynchronizationContext (我的线程是 Windows 服务的主线程)。所以我想我的问题归结为找到您在回答中提到的同步或消息传递系统。
    【解决方案2】:
    async Task RunConsumer()
    {
        foreach (var elem in _blockingCollectionFifoQueue.GetConsumingEnumerable())
        {
            try
            {
                await MethodDoingSomethingAsync(elem);
            }
            catch (MyException)
            {
                throw;
            }
        }
    }
    

    这样,来自 MethodDoingSomethingAsync 的异常将在 foreach 循环内的 catch 块中处理。它已经开箱即用。

    在调试模式下,您可能会收到一条用户未处理的异常消息。您需要禁用它或在不调试的情况下运行程序才能看到正确的结果。

    【讨论】:

    • 感谢您的回答。 RunConsumer 中抛出的异常不会传播到主线程。再加上 foreach 循环在抛出后退出,这不是我想要的。
    • 要解决第二个问题,您需要在某处收集异常并在循环完成后抛出 AggregateException。如果您不希望这样,异常可能不是这里处理错误的最佳方式。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-08
    • 1970-01-01
    • 2010-10-23
    相关资源
    最近更新 更多