我需要构建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;
答案 0 :(得分:6)
这是预期的行为。如果“下游”发生故障,则错误不会向后“向后”传播。网格期望您检测到该错误(例如,通过process_block.Completion
)并解决它。
如果要向后传播错误,如果下游块发生故障,则可能await
或process_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的义务。