簡體   English   中英

使用 TPL 數據流塊處理異常

[英]Handle exceptions with TPL Dataflow blocks

我有一個簡單的 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; }
            }); //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!");
        }
    }
}

我在網上查看了建議的方法,但沒有看到任何明顯的內容。

如果你有一個管道(或多或少),那么常用的方法是使用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來傳播異常。

開箱即用的 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);
    }
}

乍一看,如果只有一些小點(不是看你的架構)。 在我看來,你混合了一些新的和一些舊的結構。 並且有一些代碼部分是不必要的。

例如:

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(...) ,您的應用程序不會凍結。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM