其他任务完成后通知任务

时间:2016-11-22 19:09:09

标签: .net task-parallel-library tpl-dataflow

.Net TPL专家,

注意:无法使用DataFlow库;不允许使用附加组件。

我有四个任务,如下图所示:

enter image description here

  • 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可以停止(如果结果队列是也空了。)

提前致谢。

3 个答案:

答案 0 :(得分:0)

如果您还对results_queue使用BlockingCollection,则可以使用属性BlockingCollection.IsCompleted和BlockingCollection.IsAddingCompleted来实现这些通知。 流程是:

    当输入文件中没有更多记录时,
  • task1调用输入集合上的BlockingCollection.CompleteAdding()方法。
  • task2和task3检查输入集合上的regulary属性IsCompleted。当输入集合为空且生成器名为CompleteAdding()方法时,此属性为true。在此属性为true之后,任务2和3完成,他们可以在结果队列上调用CompleteAdding()方法并完成他们的工作。
  • task4可以在result_queue到达时处理它们,或者可以等待结果队列IsAddingCompleted属性变为true然后开始处理。当结果队列中IsCompleted属性为true时,task4的工作完成。

修改 我不确定您是否熟悉这些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);
        }
    }
}