使用TPL Dataflow块处理异常

时间:2019-07-09 09:27:12

标签: c# .net task-parallel-library tpl-dataflow

我有一个简单的tpl数据流,基本上可以完成一些任务。 我注意到,在任何数据块中都存在异常时,它没有被初始父块调用者捕获。 我添加了一些手动代码来检查异常,但似乎不是正确的方法。

if (readBlock.Completion.Exception != null || saveBlockJoinedProcess.Completion.Exception != null || processBlock1.Completion.Exception != null || processBlock2.Completion.Exception != null)
        {
            throw readBlock.Completion.Exception;
        }

我在线查看了建议的方法,但没有发现明显的问题。 因此,我在下面创建了一些示例代码,希望获得有关更好解决方案的指导:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;

namespace TPLDataflow
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                //ProcessB();
                ProcessA();
            }
            catch(Exception e)
            {
                Console.WriteLine("Exception in Process!");
                throw new Exception($"exception:{e}");
            }
            Console.WriteLine("Processing complete!");
            Console.ReadLine();
        }

        private static void ProcessB()
        {
            Task.WhenAll(Task.Run(() => DoSomething(1, "ProcessB"))).Wait();
        }

        private static void ProcessA()
        {
            var random = new Random();
            var readBlock = new TransformBlock<int, int>(
                    x => { try { return DoSomething(x, "readBlock"); } catch (Exception e) { throw e; } },
                    new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1 }); //1

            var braodcastBlock = new BroadcastBlock<int>(i => i); // ⬅ Here

            var processBlock1 =
                new TransformBlock<int, int>(x => DoSomethingAsync(5, "processBlock1")); //2
            var processBlock2 =
                new TransformBlock<int, int>(x => DoSomethingAsync(2, "processBlock2")); //3

            //var saveBlock =
            //    new ActionBlock<int>(
            //    x => Save(x)); //4

            var saveBlockJoinedProcess =
                new ActionBlock<Tuple<int, int>>(
                x => SaveJoined(x.Item1, x.Item2)); //4

            var saveBlockJoin = new JoinBlock<int, int>();

            readBlock.LinkTo(braodcastBlock, new DataflowLinkOptions { PropagateCompletion = true });

            braodcastBlock.LinkTo(processBlock1,
                new DataflowLinkOptions { PropagateCompletion = true }); //5

            braodcastBlock.LinkTo(processBlock2,
                new DataflowLinkOptions { PropagateCompletion = true }); //6


            processBlock1.LinkTo(
                saveBlockJoin.Target1); //7

            processBlock2.LinkTo(
                saveBlockJoin.Target2); //8

            saveBlockJoin.LinkTo(saveBlockJoinedProcess, new DataflowLinkOptions { PropagateCompletion = true });

            readBlock.Post(1); //10
                                //readBlock.Post(2); //10

            Task.WhenAll(
                        processBlock1.Completion,
                        processBlock2.Completion)
                        .ContinueWith(_ => saveBlockJoin.Complete());

            readBlock.Complete(); //12
            saveBlockJoinedProcess.Completion.Wait(); //13
            if (readBlock.Completion.Exception != null || saveBlockJoinedProcess.Completion.Exception != null || processBlock1.Completion.Exception != null || processBlock2.Completion.Exception != null)
            {
                throw readBlock.Completion.Exception;
            }
        }
        private static int DoSomething(int i, string method)
        {
            Console.WriteLine($"Do Something, callng method : { method}");
            throw new Exception("Fake Exception!");
            return i;
        }
        private static async Task<int> DoSomethingAsync(int i, string method)
        {
            Console.WriteLine($"Do SomethingAsync");
            throw new Exception("Fake Exception!");
            await Task.Delay(new TimeSpan(0,0,i));
            Console.WriteLine($"Do Something : {i}, callng method : { method}");
            return i;
        }
        private static void Save(int x)
        {

            Console.WriteLine("Save!");
        }
        private static void SaveJoined(int x, int y)
        {
            Thread.Sleep(new TimeSpan(0, 0, 10));
            Console.WriteLine("Save Joined!");
        }
    }
}

3 个答案:

答案 0 :(得分:1)

  

我在线查看了建议的方法,但是没有发现明显的问题。

如果有管道(或多或少),那么通常的方法是使用PropagateCompletion关闭管道。如果您有更复杂的拓扑,则需要手动完成块。

在您的情况下,您尝试在此处进行传播:

Task.WhenAll(
    processBlock1.Completion,
    processBlock2.Completion)
    .ContinueWith(_ => saveBlockJoin.Complete());

但是此代码不会传播异常。 processBlock1.CompletionprocessBlock2.Completion都完成时,saveBlockJoin 成功完成

更好的解决方案是使用await而不是ContinueWith

async Task PropagateToSaveBlockJoin()
{
    try
    {
        await Task.WhenAll(processBlock1.Completion, processBlock2.Completion);
        saveBlockJoin.Complete();
    }
    catch (Exception ex)
    {
        ((IDataflowBlock)saveBlockJoin).Fault(ex);
    }
}
_ = PropagateToSaveBlockJoin();

使用await会鼓励您处理异常,您可以通过将异常传递给Fault来传播异常。

答案 1 :(得分:1)

开箱即用的TPL数据流不支持在管道中向后传播错误,当块具有有限容量时,这尤其令人讨厌。在这种情况下,下游块中的错误可能导致其前面的块无限期地阻塞。我知道的唯一解决方案是使用取消功能,并在任何人失败的情况下取消所有块。这是可以完成的。首先创建一个CancellationTokenSource

var cts = new CancellationTokenSource();

然后逐个创建块,将相同的CancellationToken嵌入所有块的选项中:

var options = new ExecutionDataflowBlockOptions()
    { BoundedCapacity = 10, CancellationToken = cts.Token };

var block1 = new TransformBlock<double, double>(Math.Sqrt, options);
var block2 = new ActionBlock<double>(Console.WriteLine, options);

然后将这些块链接在一起,包括PropagateCompletion设置:

block1.LinkTo(block2, new DataflowLinkOptions { PropagateCompletion = true });

最后使用扩展方法来在发生异常的情况下触发CancellationTokenSource的取消:

block1.OnFaultedCancel(cts);
block2.OnFaultedCancel(cts);

OnFaultedCancel扩展方法如下所示:

public static class DataflowExtensions
{
    public static void OnFaultedCancel(this IDataflowBlock dataflowBlock,
        CancellationTokenSource cts)
    {
        dataflowBlock.Completion.ContinueWith(_ => cts.Cancel(), default,
            TaskContinuationOptions.OnlyOnFaulted |
            TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
    }
}

答案 2 :(得分:0)

乍一看,如果只有一点点(不看您的体系结构)。在我看来,您已经混合了一些较新的结构和较旧的结构。还有一些不必要的代码部分。

例如:

private static void ProcessB()
{
    Task.WhenAll(Task.Run(() => DoSomething(1, "ProcessB"))).Wait();
}

使用Wait()方法,如果发生任何异常,则将它们包装在System.AggregateException中。我认为这样比较好:

private static async Task ProcessBAsync()
{
    await Task.Run(() => DoSomething(1, "ProcessB"));
}

使用async-await,如果发生异常,则await语句会重新引发包装在System.AggregateException中的第一个异常。这样,您就可以尝试捕获具体的异常类型,并仅处理您真正可以处理的情况。

另一部分是代码的这一部分:

private static void ProcessA()
        {
            var random = new Random();
            var readBlock = new TransformBlock<int, int>(
                    x => 
                    { 
                    try { return DoSomething(x, "readBlock"); } 
                    catch (Exception e) 
                    { 
                    throw e; 
                    } 
                    },
                    new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1 }); //1

为什么只捕获异常才能将其抛出?在这种情况下,try-catch是多余的。

这是这里:

private static void SaveJoined(int x, int y)
{
    Thread.Sleep(new TimeSpan(0, 0, 10));
    Console.WriteLine("Save Joined!");
}

使用await Task.Delay(....)更好。使用Task.Delay(...),您的应用程序将不会冻结。