繁体   English   中英

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

[英]C# Multi-threading, wait for all task to complete in a situation when new tasks are being constantly added

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

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

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

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

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)不断地合并处理器)


注释中要求的其他上下文信息。 我认为这与问题无关。

这段代码在Web搜寻器中使用。 搜寻器扫描页面内容并查找两种类型的信息。 数据页和链接页。 将扫描数据页面并收集数据,将扫描链接页面并从中收集更多链接。

当每个任务进行活动并查找更多链接时,它们会将链接添加到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);
        }
    }
}

创建任务时,为什么不编写一个可以根据需要生成任务的函数呢? 这样,您可以只使用Task.WhenAll等待它们完成,或者我错过了重点吗? 看到这里工作

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的组合将使您非常接近所需的内容。 你可以从这里开始


无论哪种方式,我建议您都包含一个接受CancellationToken或两个的规定。

我认为可以使用非常基本的设置使用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;

您可以检查完成的时刻,例如,如果bufferCount属性 transformInputCount transformCurrentDegreeOfParallelism (这是TransformManyBlock内部属性)都等于0

但是,我建议您在此处实施一些其他逻辑来确定电流互感器的数量,因为使用内部逻辑并不是一个很好的解决方案。 至于取消管​​道,您可以创建一个带有CancellationTokenTPL块,该TPL块可以全部CancellationToken ,也可以每个块专用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM