.Net TPL专家,
注意:无法使用DataFlow库;不允许使用附加组件。
我有四个任务,如下图所示:
task_1(data_producer) - >从大文件中读取记录(> 500000条记录)并将记录添加到BlockingCollection
task_2,task_3(data_consumers) - >这些任务中的每一个都从BlockingCollection中获取记录。每个任务对从BlockingCollection(网络相关)获取的记录执行一些工作,并且在完成时,每个任务可以将记录添加到结果队列。处理顺序并不重要。
task_4(结果处理器) - >从results_queue获取记录并写入输出文件。
然后我等待任务完成,即:
Task.WhenAll( t1, t2, t3, t4 )
所以我有一个生产者任务,MULTIPLE消费者任务和一个保存结果的任务。
我的问题是:
如何在任务2和3完成时通知任务4,以便任务4也知道何时结束?
我发现了很多以线性“管道”方式将数据从一个任务“移动”到另一个任务的例子,但没有找到任何说明上述内容的例子。也就是说,当任务2和3完成时如何通知任务4,以便它知道何时完成。
我最初的想法是用任务4“注册”任务2和3并简单地监视每个注册任务的状态 - 当任务2和3不再运行时,任务4可以停止(如果结果队列是也空了。)
提前致谢。
答案 0 :(得分:0)
如果您还对results_queue使用BlockingCollection,则可以使用属性BlockingCollection.IsCompleted和BlockingCollection.IsAddingCompleted来实现这些通知。 流程是:
修改强> 我不确定您是否熟悉这些IsCompleted和IsAddingCompleted属性。它们是不同的,非常适合您的情况。除了BlockingCollection属性之外,我认为您不需要任何其他同步元素。请询问是否需要其他说明!
BlockingCollection<int> inputQueue;
BlockingCollection<int> resultQueue;
public void StartTasks()
{
inputQueue = new BlockingCollection<int>();
resultQueue = new BlockingCollection<int>();
Task task1 = Task.Run(() => Task1());
Task task2 = Task.Run(() => Task2_3());
Task task3 = Task.Run(() => Task2_3());
Task[] tasksInTheMiddle = new Task[] { task2, task3 };
Task waiting = Task.Run(() => Task.WhenAll(tasksInTheMiddle).ContinueWith(x => resultQueue.CompleteAdding()));
Task task4 = Task.Run(() => Task4());
//Waiting for tasks to finish
}
private void Task1()
{
while(true)
{
int? input = ReadFromInputFile();
if (input != null)
{
inputQueue.Add((int)input);
}
else
{
inputQueue.CompleteAdding();
break;
}
}
}
private void Task2_3()
{
while(inputQueue.IsCompleted)
{
int input = inputQueue.Take();
resultQueue.Add(input);
}
}
private void Task4()
{
while(resultQueue.IsCompleted)
{
int result = resultQueue.Take();
WriteToOutputFile(result);
}
}
答案 1 :(得分:0)
您所描述的任务可以很好地适合TPL Dataflow library TPL
本身的小插件(它可以通过nuget package包含在项目中,.NET 4.5 < strong> 支持),您只需轻松介绍类似的流程(基于BroadcastBlock
的评论代码已更新):
var buffer = new BroadcastBlock<string>();
var consumer1 = new TransformBlock<string, string>(s => { /* your action here for a string */});
var consumer2 = new TransformBlock<string, string>(s => { /* your action here for a string */});
var resultsProcessor = new ActionBlock<string>(s => { /* your logging logic here */ });
不确定您的解决方案逻辑,所以我认为您只需在此操作字符串。你应该asynchronously send第一个块的所有传入数据(如果你Post
你的数据,如果缓冲区过载,消息将被丢弃),并且彼此之间链接块,如下所示:
buffer.LinkTo(consumer1, new DataflowLinkOptions { PropagateCompletion = true });
buffer.LinkTo(consumer2, new DataflowLinkOptions { PropagateCompletion = true });
consumer1.LinkTo(resultsProcessor, new DataflowLinkOptions { PropagateCompletion = true });
consumer2.LinkTo(resultsProcessor, new DataflowLinkOptions { PropagateCompletion = true });
foreach (var s in IncomingData)
{
await buffer.SendAsync(s);
}
buffer.Complete();
如果您的消费者同时处理所有项目,那么您应该使用BroadcastBlock
(可能会出现一些issues about the guaranteed delivery),其他选项是过滤消费者的消息(可能是来自消息ID的余数来自消费者的数量),但在这种情况下,你应该链接到另一个消费者,它将“捕获”所有消息,这些消息由于某种原因没有被消费。
正如您所看到的,块之间的链接是使用完全传播创建的,因此在此之后您只需附加resultsProcessor
的{{3}}任务属性:
resultsProcessor.Completion.ContinueWith(t => { /* Processing is complete */ });
答案 2 :(得分:0)
这是对Thomas已经说过的内容的一点延伸。
使用BlockingCollection
,您可以在其上调用GetConsumingEnumerable()
,并将其视为正常的foreach循环。这将让你的任务“自然地”结束。您唯一需要做的就是添加一个额外的任务来监视任务2和3,以查看它们何时结束并调用它们的完整添加。
private BlockingCollection<Stage1> _stageOneBlockingCollection = new BlockingCollection<Stage1>();
private BlockingCollection<Stage2> _stageTwoBlockingCollection = new BlockingCollection<Stage2>();
Task RunProcess()
{
Task1Start();
var t2 = Stage2Start();
var t3 = Stage2Start();
Stage2MonitorStart(t2,t3);
retrun Task4Start();
}
public void Task1Start()
{
Task.Run(()=>
{
foreach(var item in GetFileSource())
{
var processedItem = Process(item);
_stageOneBlockingCollection.Add(processedItem);
}
_stageOneBlockingCollection.CompleteAdding();
}
}
public Task Stage2Start()
{
return Task.Run(()=>
{
foreach(var item in _stageOneBlockingCollection.GetConsumingEnumerable())
{
var processedItem = ProcessStage2(item);
_stageTwoBlockingCollection.Add(processedItem);
}
}
}
void Stage2MonitorStart(params Task[] tasks)
{
//Once all tasks complete mark the collection complete adding.
Task.WhenAll(tasks).ContinueWith(t=>_stageTwoBlockingCollection.CompleteAdding());
}
public Task Stage4Start()
{
return Task.Run(()=>
{
foreach(var item in _stageTwoBlockingCollection.GetConsumingEnumerable())
{
var processedItem = ProcessStage4(item);
WriteToOutputFile(processedItem);
}
}
}