具有有界容量的变换块中的TPL数据流异常

时间:2014-02-06 12:47:13

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

我需要构建TPL数据流管道,它将处理大量消息。因为有很多消息,我不能简单Post将它们放入BufferBlock的无限队列中,否则我将面临内存问题。所以我想使用BoundedCapacity = 1选项来禁用队列并使用MaxDegreeOfParallelism来使用并行任务处理,因为我的TransformBlock可能需要一些时间来处理每条消息。我还使用PropagateCompletion来完成所有操作并且无法在管道中传播。

但是,当第一条消息发生错误时,我正面临错误处理的问题:调用await SendAsync只需将我的应用程序切换到无限等待。

我已将案例简化为示例控制台应用程序:

var data_buffer = new BufferBlock<int> (new DataflowBlockOptions
                                        {
                                            BoundedCapacity = 1
                                        });

var process_block = new ActionBlock<int> (x => { throw new InvalidOperationException (); },
                                            new ExecutionDataflowBlockOptions
                                            {
                                                MaxDegreeOfParallelism = 2,
                                                BoundedCapacity = 1
                                            });

data_buffer.LinkTo (process_block, new DataflowLinkOptions { PropagateCompletion = true });


for (var k = 1; k <= 5; k++)
{
    await data_buffer.SendAsync (k);
    Console.WriteLine ("Send: {0}", k);
}

data_buffer.Complete ();

await process_block.Completion;

2 个答案:

答案 0 :(得分:6)

这是预期的行为。如果“下游”发生故障,则错误不会向后“向后”传播。网格期望您检测到该错误(例如,通过process_block.Completion)并解决它。

如果要向后传播错误,如果下游块发生故障,则可能awaitprocess_block.Completion上的{{1}}继续faults上游块。< / p>

请注意,这不是唯一可行的解​​决方案;您可能希望重建网格的该部分或将源链接到备用目标。源块没有出现故障,因此可以继续使用已修复的网格进行处理。

答案 1 :(得分:0)

不幸的是,没有仅通过配置块来向后传播完成的内置方法。必须手动完成。一种方法是为每个前向传播链路建立后向传播链路。当您有一个由2-3个块组成的小型管道时,它是快速而容易的,但是随着管道的增长,它变得更加麻烦和容易出错:

data_buffer.LinkTo(process_block,
    new DataflowLinkOptions { PropagateCompletion = true });
PropagateFailure(process_block, data_buffer); // Propagate backwards

public static async void PropagateFailure(IDataflowBlock block1, IDataflowBlock block2)
{
    try { await block1.Completion.ConfigureAwait(false); } catch { }
    if (block1.Completion.IsFaulted) block2.Fault(block1.Completion.Exception);
}

具有更集成的API的相同想法:

public static async void BidirectionalLinkTo<T>(this ISourceBlock<T> source,
    ITargetBlock<T> target)
{
    source.LinkTo(target, new DataflowLinkOptions { PropagateCompletion = true });
    try { await target.Completion.ConfigureAwait(false); } catch { }
    if (target.Completion.IsFaulted) source.Fault(target.Completion.Exception);
}

data_buffer.BidirectionalLinkTo(process_block);

另一种方法是确保在任何块失败的情况下取消整个管道。这可以通过使用来自同一源的CancellationToken配置所有块,并将处理程序附加到每个块的完成来取消源来完成:

var cts = new CancellationTokenSource();

var data_buffer = new BufferBlock<int>(new DataflowBlockOptions
{
    BoundedCapacity = 1,
    CancellationToken = cts.Token
});
//...more blocks configured with the same cts.Token

OnErrorCancel(data_buffer, cts);
OnErrorCancel(process_block, cts);
//...

async void OnErrorCancel(IDataflowBlock block, CancellationTokenSource cts)
{
    try { await block.Completion.ConfigureAwait(false); } catch { }
    if (block.Completion.IsFaulted) cts.Cancel();
}

此解决方案吸引力降低的原因在于,创建CancellationTokenSource还会产生Dispose which is not always trivial to do的义务。