在Parallel方法中抛出原始异常而不是聚合异常

时间:2018-02-27 20:03:52

标签: c# parallel-processing task-parallel-library

我在Parallel.Invoke调用中有两个CPU密集型方法:

Parallel.Invoke(
    () => { GetMaxRateDict(tradeOffObj); },
    () => { GetMinRateDict(tradeOffObj); }
);

对于MCVE,假设:

public void GetMaxRateDict(object junk)
{
    throw new Exception("Max exception raised, do foo...");
}
public void GetMinRateDict(object moreJunk)
{
    throw new Exception("Min exception raised, do bar...")
}

我在每个内部方法中抛出不同的异常。但是,如果其中一个被抛出,则Parallel包装器会抛出一个更通用的异常:“发生了一个或多个错误”,这个错误足以在我的UI层中显示。

我可以以某种方式获取原始异常并将其丢弃吗?

我希望Parallel任务在可能的情况下完全停止以引发内部异常,但如果这不可能,那么至少能够在两个方法完成后提升它是我需要的。感谢。

2 个答案:

答案 0 :(得分:1)

  

我可以以某种方式获取原始异常并将其丢弃吗?

“它”意味着只有例外。即使这可能是正确的,因为您并行执行操作,即使您在第一个异常后尝试取消其他操作,也不能100%排除多个操作抛出异常的可能性。如果你对此感到满意,我们可以假设我们只期待一个例外,我们只能抓一个例外。 (如果在抛出异常后允许其他调用继续,则有两个异常的可能性会增加。)

您可以使用取消令牌。如果下面的一个调用抛出异常,它应该捕获该异常,将它放在变量或队列中,然后调用

source.Cancel;

这样做会导致整个Parallel.Invoke抛出OperationCanceledException。您可以捕获该异常,检索已设置的异常,然后重新抛出该异常。

我将采用另一个答案的建议ConcurrentQueue只是为了练习,因为我认为我们不能排除第二个线程在被作为例外之前抛出异常的可能性很小的可能性取消。

这开始似乎很小,但最终它变得如此复杂,我把它分成了自己的类。这让我怀疑我的方法是否是不必要的复杂。主要目的是防止凌乱的取消逻辑污染您的GetMaxRateDictGetMinRateDict方法。

除了保持原始方法未被污染和可测试之外,此类本身也是可测试的。

我想我会从其他回答中找出这是一种体面的方法,还是有更简单的方法。 我不能说我对这个解决方案特别兴奋。我只是觉得它很有意思,想要写一些能够满足你要求的东西。

public class ParallelInvokesMultipleInvocationsAndThrowsOneException //names are hard
{
    public void InvokeActions(params Action[] actions)
    {
        using (CancellationTokenSource source = new CancellationTokenSource())
        {
            // The invocations can put their exceptions here.
            var exceptions = new ConcurrentQueue<Exception>();

            var wrappedActions = actions
                .Select(action => new Action(() =>
                    InvokeAndCancelOthersOnException(action, source, exceptions)))
                .ToArray();
            try
            {
                Parallel.Invoke(new ParallelOptions{CancellationToken = source.Token}, 
                    wrappedActions)
            }
            // if any of the invocations throw an exception, 
            // the parallel invocation will get canceled and 
            // throw an OperationCanceledException;
            catch (OperationCanceledException ex)
            {
                Exception invocationException;
                if (exceptions.TryDequeue(out invocationException))
                {
                    //rethrow however you wish.
                    throw new Exception(ex.Message, invocationException);
                }
                // You shouldn't reach this point, but if you do, throw something else.
                // In the unlikely but possible event that you get more
                // than one exception, you'll lose all but one.
            }
        }
    }

    private void InvokeAndCancelOthersOnException(Action action,
        CancellationTokenSource cancellationTokenSource,
        ConcurrentQueue<Exception> exceptions)
    {
        // Try to invoke the action. If it throws an exception,
        // capture the exception and then cancel the entire Parallel.Invoke.
        try
        {
            action.Invoke();
        }
        catch (Exception ex)
        {
            exceptions.Enqueue(ex);
            cancellationTokenSource.Cancel();
        }
    }
}

然后使用

var thingThatInvokes = new ParallelInvokesMultipleInvocationsAndThrowsOneException();
thingThatInvokes.InvokeActions(
    ()=> GetMaxRateDict(tradeOffObj),
    () => GetMinRateDict(tradeOffObj));

如果它抛出异常,它将是一个调用失败的单个异常,而不是聚合异常。

答案 1 :(得分:0)

不确定给定的示例是否会回答您的问题,但它可能会改善整体解决方案:

private static void ProcessDataInParallel(byte[] data)

{
    // use ConcurrentQueue to enable safe enqueueing from multiple threads.
    var exceptions = new ConcurrentQueue<Exception>();

    // execute the complete loop and capture all exceptions
    Parallel.ForEach(data, d =>
    {
        try
        {
            // something that might fail goes here...
        }
        // accumulate stuff, be patient ;)
        catch (Exception e) { exceptions.Enqueue(e); }
    });

    // check whether something failed?..
    if (exceptions.Count > 0) // do whatever you like ;
}

这种方法在将不同类型的异常收集到不同队列(如果需要)或重新抛出聚合异常方面提供了额外的自由(这样就不会产生敏感信息,或者您可以通过用户友好的方式传达特殊异常可能的原因描述等)。

通常,这是使用并行化进行异常管理的正确方法。不仅仅是在C#中。