如何在完成两个转换完成后重新编写代码完成的代码?我认为完成意味着它被标记为完成并且“出队列”是空的?
public Test()
{
broadCastBlock = new BroadcastBlock<int>(i =>
{
return i;
});
transformBlock1 = new TransformBlock<int, string>(i =>
{
Console.WriteLine("1 input count: " + transformBlock1.InputCount);
Thread.Sleep(50);
return ("1_" + i);
});
transformBlock2 = new TransformBlock<int, string>(i =>
{
Console.WriteLine("2 input count: " + transformBlock1.InputCount);
Thread.Sleep(20);
return ("2_" + i);
});
processorBlock = new ActionBlock<string>(i =>
{
Console.WriteLine(i);
});
//Linking
broadCastBlock.LinkTo(transformBlock1, new DataflowLinkOptions { PropagateCompletion = true });
broadCastBlock.LinkTo(transformBlock2, new DataflowLinkOptions { PropagateCompletion = true });
transformBlock1.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
transformBlock2.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
}
public void Start()
{
const int numElements = 100;
for (int i = 1; i <= numElements; i++)
{
broadCastBlock.SendAsync(i);
}
//mark completion
broadCastBlock.Complete();
processorBlock.Completion.Wait();
Console.WriteLine("Finished");
Console.ReadLine();
}
}
我编辑了代码,为每个变换块添加了输入缓冲区计数。显然,所有100个项目都流式传输到每个变换块。但是只要其中一个transformblock完成,处理器块就不再接受任何项目,而不完整的transformblock的输入缓冲区只是刷新输入缓冲区。
答案 0 :(得分:29)
问题正是casperOne在答案中所说的。第一个转换块完成后,处理器块进入“完成模式”:它将处理其输入队列中的剩余项目,但不会接受任何新项目。
除了将处理器块分成两部分之外,还有一个更简单的解决方法:不要设置PropagateCompletion
,而是在两个转换块完成时手动设置处理器块的完成:
Task.WhenAll(transformBlock1.Completion, transformBlock2.Completion)
.ContinueWith(_ => processorBlock.Complete());
答案 1 :(得分:24)
这里的问题是,每次调用PropagateCompletion
property来链接块以及转换块中等待时间不同时,您都要设置LinkTo
method。
来自Complete
method(强调我的)IDataflowBlock
interface的文档:
向IDataflowBlock发出信号,表示不应接受,也不再生成消息,也不再使用推迟消息。
因为你在每个TransformBlock<TInput, TOutput>
个实例中错开了等待时间,transformBlock2
(等待20毫秒)在transformBlock1
之前完成(等待50毫秒)。 transformBlock2
首先完成,然后将信号发送到processorBlock
,然后说“我不接受任何其他内容”(并且transformBlock1
尚未生成所有消息)。< / p>
请注意,transformBlock1
之前transformBlock1
的处理不是绝对保证;线程池(假设您正在使用默认调度程序)将以不同的顺序处理任务是可行的(但很可能不会,因为一旦完成20 ms的项目,它将从队列中窃取工作)。< / p>
您的管道如下所示:
broadcastBlock
/ \
transformBlock1 transformBlock2
\ /
processorBlock
为了解决这个问题,您希望拥有一个如下所示的管道:
broadcastBlock
/ \
transformBlock1 transformBlock2
| |
processorBlock1 processorBlock2
只需创建两个单独的ActionBlock<TInput>
实例即可实现,如下所示:
// The action, can be a method, makes it easier to share.
Action<string> a = i => Console.WriteLine(i);
// Create the processor blocks.
processorBlock1 = new ActionBlock<string>(a);
processorBlock2 = new ActionBlock<string>(a);
// Linking
broadCastBlock.LinkTo(transformBlock1,
new DataflowLinkOptions { PropagateCompletion = true });
broadCastBlock.LinkTo(transformBlock2,
new DataflowLinkOptions { PropagateCompletion = true });
transformBlock1.LinkTo(processorBlock1,
new DataflowLinkOptions { PropagateCompletion = true });
transformBlock2.LinkTo(processorBlock2,
new DataflowLinkOptions { PropagateCompletion = true });
然后您需要等待两个处理器块而不是一个:
Task.WhenAll(processorBlock1.Completion, processorBlock2.Completion).Wait();
此处非常重要说明;在创建ActionBlock<TInput>
时,默认设置是将传递给它的MaxDegreeOfParallelism
property实例上的ExecutionDataflowBlockOptions
设置为1。
这意味着您传递给ActionBlock<TInput>
的{{3}}的调用是线程安全的,一次只执行一次。
因为您现在有两个 ActionBlock<TInput>
个实例指向同一个Action<T>
委托,所以不能保证线程安全。
如果您的方法是线程安全的,那么您不必执行任何操作(这将允许您将MaxDegreeOfParallelism
属性设置为Action<T>
delegate,因为没有理由阻止。< / p>
如果不是线程安全的,并且您需要保证它,则需要使用传统的同步原语,例如DataflowBlockOptions.Unbounded
。
在这种情况下,你会这样做(尽管显然不需要,因为lock
statement上的WriteLine
method是线程安全的):
// The lock.
var l = new object();
// The action, can be a method, makes it easier to share.
Action<string> a = i => {
// Ensure one call at a time.
lock (l) Console.WriteLine(i);
};
// And so on...
答案 2 :(得分:8)
svick答案的补充:为了与使用PropagateCompletion选项获得的行为一致,您还需要在前一个块出现故障的情况下转发异常。像下面这样的扩展方法也会解决这个问题:
public static void CompleteWhenAll(this IDataflowBlock target, params IDataflowBlock[] sources) {
if (target == null) return;
if (sources.Length == 0) { target.Complete(); return; }
Task.Factory.ContinueWhenAll(
sources.Select(b => b.Completion).ToArray(),
tasks => {
var exceptions = (from t in tasks where t.IsFaulted select t.Exception).ToList();
if (exceptions.Count != 0) {
target.Fault(new AggregateException(exceptions));
} else {
target.Complete();
}
}
);
}
答案 3 :(得分:1)
其他答案非常清楚为什么当一个块有两个以上的源时,PropagateCompletion =真正搞乱。
要提供问题的简单解决方案,您可能需要查看一个开源库DataflowEx,它可以通过内置更智能的完成规则解决此类问题。 (它在内部使用TPL Dataflow链接但支持复杂的完成传播。该实现看起来与WhenAll类似,但也处理动态链接添加。请检查Dataflow.RegisterDependency()和TaskEx.AwaitableWhenAll()以获取详细信息。)
我稍微更改了您的代码,以便使用DataflowEx完成所有工作:
public CompletionDemo1()
{
broadCaster = new BroadcastBlock<int>(
i =>
{
return i;
}).ToDataflow();
transformBlock1 = new TransformBlock<int, string>(
i =>
{
Console.WriteLine("1 input count: " + transformBlock1.InputCount);
Thread.Sleep(50);
return ("1_" + i);
});
transformBlock2 = new TransformBlock<int, string>(
i =>
{
Console.WriteLine("2 input count: " + transformBlock2.InputCount);
Thread.Sleep(20);
return ("2_" + i);
});
processor = new ActionBlock<string>(
i =>
{
Console.WriteLine(i);
}).ToDataflow();
/** rather than TPL linking
broadCastBlock.LinkTo(transformBlock1, new DataflowLinkOptions { PropagateCompletion = true });
broadCastBlock.LinkTo(transformBlock2, new DataflowLinkOptions { PropagateCompletion = true });
transformBlock1.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
transformBlock2.LinkTo(processorBlock, new DataflowLinkOptions { PropagateCompletion = true });
**/
//Use DataflowEx linking
var transform1 = transformBlock1.ToDataflow();
var transform2 = transformBlock2.ToDataflow();
broadCaster.LinkTo(transform1);
broadCaster.LinkTo(transform2);
transform1.LinkTo(processor);
transform2.LinkTo(processor);
}
完整代码为here。
免责声明:我是DataflowEx的作者,该版本是根据MIT许可证发布的。
答案 4 :(得分:0)
这是一种在功能上与pkt的CompleteWhenAll
方法等效的方法,但是代码略少:
public static void PropagateCompletionOfAll(IDataflowBlock[] sources,
IDataflowBlock target)
{
_ = Task.WhenAll(sources.Select(b => b.Completion)).ContinueWith(t =>
{
if (t.IsFaulted)
{
target.Fault(t.Exception);
}
else
{
target.Complete();
}
}, TaskScheduler.Default);
}
用法示例:
PropagateCompletionOfAll(new[] { transformBlock1, transformBlock2 }, processorBlock);
您可以考虑添加条件:else if (t.IsCanceled) { target.Fault(new OperationCanceledException()); }
,但这与内置完成传播的工作方式不一致。