簡體   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