C#多线程,等待所有任务在不断添加新任务的情况下完成

时间:2017-03-27 13:32:06

标签: c# multithreading parallel-processing task-parallel-library

我的情况是不断生成新任务并将其添加到ConcurrentBag<Tasks>

我需要等待所有任务完成。

通过ConcurrentBag等待WaitAll中的所有任务是不够的,因为在前一次等待完成时任务数量会增加。

目前我正在以下列方式等待:

private void WaitAllTasks()
{
    while (true)
    {
        int countAtStart = _tasks.Count();
        Task.WaitAll(_tasks.ToArray());

        int countAtEnd = _tasks.Count();
        if (countAtStart == countAtEnd)
        {
            break;
        }

        #if DEBUG
        if (_tasks.Count() > 100)
        {
            tokenSource.Cancel();
            break;
        }
        #endif
    }
}

我对while(true)解决方案不满意。

任何人都可以提出一种更有效的方法来做到这一点(无需使用while(true)不断集中处理器)

评论中要求的其他上下文信息。我不认为这与这个问题有关。

这段代码用于网络爬虫。爬网程序扫描页面内容并查找两种类型的信息。数据页面和链接页面。将扫描数据页并收集数据,扫描链接页面并从中收集更多链接。

由于每项任务都会进行活动并找到更多链接,因此他们会将链接添加到EventList。列表中有一个事件OnAdd(下面的代码),用于触发其他任务扫描新添加的URL。等等。

当没有更多正在运行的任务时,作业完成(因此不再添加链接)并且所有项目都已处理完毕。

public IEventList<ISearchStatus> CurrentLinks { get; private set; }
public IEventList<IDataStatus> CurrentData { get; private set; }
public IEventList<System.Dynamic.ExpandoObject> ResultData { get; set; }
private readonly ConcurrentBag<Task> _tasks = new ConcurrentBag<Task>();

private readonly CancellationTokenSource tokenSource = new CancellationTokenSource();
private readonly CancellationToken token;

public void Search(ISearchDefinition search)
{
    CurrentLinks.OnAdd += UrlAdded;
    CurrentData.OnAdd += DataUrlAdded;

    var status = new SearchStatus(search);

    CurrentLinks.Add(status);

    WaitAllTasks();

    _exporter.Export(ResultData as IList<System.Dynamic.ExpandoObject>);
}

private void DataUrlAdded(object o, EventArgs e)
{
    var item = o as IDataStatus;
    if (item == null)
    {
        return;
    }

    _tasks.Add(Task.Factory.StartNew(() => ProcessObjectSearch(item), token));
}

private void UrlAdded(object o, EventArgs e)
{
    var item = o as ISearchStatus;
    if (item==null)
    {
        return;
    }

    _tasks.Add(Task.Factory.StartNew(() => ProcessFollow(item), token));
    _tasks.Add(Task.Factory.StartNew(() => ProcessData(item), token));
}

 public class EventList<T> : List<T>, IEventList<T>
{
    public EventHandler OnAdd { get; set; }
    private readonly object locker = new object();
    public new void Add(T item)
    {
        //lock (locker)
        {
            base.Add(item);
        }
        OnAdd?.Invoke(item, null);
    }

    public new bool Contains(T item)
    {
        //lock (locker) 
        {
            return base.Contains(item);
        }
    }
}

2 个答案:

答案 0 :(得分:0)

为什么不编写一个在创建任务时根据需要生成任务的函数?这样你就可以使用Task.WhenAll等待它们完成,或者我错过了这一点? See this working here

using System;
using System.Threading.Tasks;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        try
        {
            Task.WhenAll(GetLazilyGeneratedSequenceOfTasks()).Wait();   
            Console.WriteLine("Fisnished.");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);  
        }   
    }

    public static IEnumerable<Task> GetLazilyGeneratedSequenceOfTasks()
    {
        var random =  new Random();
        var finished = false;
        while (!finished)
        {
            var n = random.Next(1, 2001);
            if (n < 50)
            {
                finished = true;
            }

            if (n > 499)
            {
                yield return Task.Delay(n);
            }

            Task.Delay(20).Wait();              
        }

        yield break;
    }
}

或者,如果您的问题不像我的回答所暗示的那样微不足道,我会考虑使用TPL Dataflow的网格。 BufferBlockActionBlock的组合可以让您非常接近您的需求。你可以start here

无论哪种方式,我都建议您添加一项接受CancellationToken或两项的规定。

答案 1 :(得分:0)

我认为可以使用基本设置的TPL Dataflow库来完成此任务。您需要TransformManyBlock<Task, IEnumerable<DataTask>>ActionBlock(可能更多)进行实际数据处理,如下所示:

// queue for a new urls to parse
var buffer = new BufferBlock<ParseTask>();

// parser itself, returns many data tasks from one url
// similar to LINQ.SelectMany method
var transform = new TransformManyBlock<ParseTask, DataTask>(task =>
{
    // get all the additional urls to parse
    var parsedLinks = GetLinkTasks(task);
    // get all the data to parse
    var parsedData = GetDataTasks(task);

    // setup additional links to be parsed
    foreach (var parsedLink in parsedLinks)
    {
        buffer.Post(parsedLink);
    }

    // return all the data to be processed
    return parsedData;
});

// actual data processing
var consumer = new ActionBlock<DataTask>(s => ProcessData(s));

之后你需要链接每个之间的块:

buffer.LinkTo(transform, new DataflowLinkOptions { PropagateCompletion = true });
transform.LinkTo(consumer, new DataflowLinkOptions { PropagateCompletion = true });

现在你有一个很好的管道,它将在后台执行。目前您意识到所需的一切都已解析,您只需调用Complete方法获取一个块,以便它停止接受新闻消息。在buffer变空之后,它会将管道中的完成传播到transform块,这会将其传播给消费者,您需要等待Completion任务:

// no additional links would be accepted
buffer.Complete();
// after all the tasks are done, this will get fired
await consumer.Completion;

您可以查看完成时间,例如,如果buffer'Count属性 transform'InputCount transform'CurrentDegreeOfParallelism(这是TransformManyBlock的内部属性)等于0

但是,我建议你在这里实现一些额外的逻辑来确定当前的变压器数量,因为使用内部逻辑不是一个很好的解决方案。至于取消管​​道,您可以使用TPL创建一个CancellationToken块,或者为所有块创建一个块,或者为每个块创建一个专用块,以便取消开箱即用。