简体   繁体   English

使用 TPL 数据流块处理异常

[英]Handle exceptions with TPL Dataflow blocks

I have a simple tpl data flow which basically does some tasks.我有一个简单的 tpl 数据流,它基本上可以完成一些任务。 I noticed when there is an exception in any of the datablocks, it wasn't getting caught in the initial parent block caller.我注意到当任何数据块中出现异常时,它不会被初始父块调用者捕获。 I have added some manual code to check for exception but doesn't seem the right approach.我添加了一些手动代码来检查异常,但似乎不是正确的方法。

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

I had a look online to see what's a suggested approach but didn't see anything obvious.我在网上查看了建议的方法,但没有看到任何明显的内容。 So I created some sample code below and was hoping to get some guidance on a better solution:所以我在下面创建了一些示例代码,并希望得到一些关于更好解决方案的指导:

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; }
            }); //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!");
        }
    }
}

I had a look online to see what's a suggested approach but didn't see anything obvious.我在网上查看了建议的方法,但没有看到任何明显的内容。

If you have a pipeline (more or less), then the common approach is to use PropagateCompletion to shut down the pipe.如果你有一个管道(或多或少),那么常用的方法是使用PropagateCompletion来关闭管道。 If you have more complex topologies, then you would need to complete blocks by hand.如果您有更复杂的拓扑,则需要手动完成块。

In your case, you have an attempted propagation here:在您的情况下,您尝试在此处进行传播:

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

But this code will not propagate exceptions.但是这段代码不会传播异常。 When both processBlock1.Completion and processBlock2.Completion complete, saveBlockJoin is completed successfully .processBlock1.CompletionprocessBlock2.Completion完成时, saveBlockJoin成功完成。

A better solution would be to use await instead of ContinueWith :更好的解决方案是使用await而不是ContinueWith

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

Using await encourages you to handle exceptions, which you can do by passing them to Fault to propagate the exception.使用await鼓励您处理异常,您可以通过将它们传递给Fault来传播异常。

Propagating errors backward in the pipeline is not supported in the TPL Dataflow out of the box, which is especially annoying when the blocks have a bounded capacity.开箱即用的 TPL 数据流不支持在管道中向后传播错误,这在块具有有限容量时尤其烦人。 In this case an error in a block downstream may cause the blocks in front of it to block indefinitely.在这种情况下,下游块中的错误可能会导致其前面的块无限期地阻塞。 The only solution I know is to use the cancellation feature, and cancel all blocks in case anyone fails.我知道的唯一解决方案是使用取消功能,并在任何人失败时取消所有块。 Here is how it can be done.这是如何做到的。 First create a CancellationTokenSource :首先创建一个CancellationTokenSource

var cts = new CancellationTokenSource();

Then create the blocks one by one, embedding the same CancellationToken in the options of all of them:然后一一创建块,在所有块的选项中嵌入相同的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);

Then link the blocks together, including the PropagateCompletion setting:然后将块链接在一起,包括PropagateCompletion设置:

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

Finally use an extension method to trigger the cancellation of the CancellationTokenSource in case of an exception:最后使用一个扩展方法来触发取消CancellationTokenSource的异常情况:

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

The OnFaultedCancel extension method is shown below: 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);
    }
}

at the first look, if have only some minor points (not looking at your architecture).乍一看,如果只有一些小点(不是看你的架构)。 it seems to me that you have mixed some newer and some older constructs.在我看来,你混合了一些新的和一些旧的结构。 and there are some code parts which are unnecessary.并且有一些代码部分是不必要的。

for example:例如:

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

using the Wait()-method, if any exceptions happen, they will be wrapped in a System.AggregateException.使用 Wait() 方法,如果发生任何异常,它们将被包装在 System.AggregateException 中。 in my opinion, this is better:在我看来,这更好:

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

using async-await, if an exception occurs, the await statement rethrows the first exception which is wrapped in the System.AggregateException.使用 async-await,如果发生异常,await 语句将重新抛出包含在 System.AggregateException 中的第一个异常。 This allows you to try-catch for concrete exception types and handle only cases you really can handle.这允许您尝试捕获具体的异常类型并仅处理您真正可以处理的情况。

another thing is this part of your code:另一件事是您的代码的这一部分:

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

Why catch an exception only to rethrow it?为什么捕获异常只是为了重新抛出它? in this case, the try-catch is redundant.在这种情况下,try-catch 是多余的。

And this here:这在这里:

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

It is much better to use await Task.Delay(....) .最好使用 await Task.Delay(....) Using Task.Delay(...) , your application will not freeze.使用Task.Delay(...) ,您的应用程序不会冻结。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM