限制通过并行任务库运行的活动任务数的最佳方法

时间:2012-06-21 13:10:52

标签: c# .net task-parallel-library

考虑一个队列,其中包含需要处理的批次作业。队列限制一次只能获得1个工作,无法知道有多少工作。这些作业需要花费10秒才能完成,并且需要大量等待来自Web服务的响应,因此不受CPU限制。

如果我使用这样的东西

while (true)
{
   var job = Queue.PopJob();
   if (job == null)
      break;
   Task.Factory.StartNew(job.Execute); 
}

然后,它会以比完成它们更快的速度从队列中快速弹出作业,耗尽内存并瘫痪。 >。<

我不能使用(我不认为)ParallelOptions.MaxDegreeOfParallelism因为我不能使用Parallel.Invoke或Parallel.ForEach

我找到了3个替代品

  1. 替换Task.Factory.StartNew
    Task task = new Task(job.Execute,TaskCreationOptions.LongRunning)
    task.Start();
    

    这似乎在某种程度上解决了问题,但我不是clear exactly what this is doing,如果这是最好的方法。

  2. 创建custom task scheduler that limits the degree of concurrency

  3. 使用BlockingCollection之类的内容在开始时将作业添加到集合中,并在完成后删除以限制可以运行的数字。

  4. #1我必须相信自己做出了正确的决定,#2 /#3我必须计算出自己可以运行的最大任务数。

    我是否理解正确 - 这是更好的方式,还是有另一种方式?

    编辑 - 这是我从下面的答案,生产者 - 消费者模式中得出的结论。

    总体吞吐量目标不是要使作业更快地出列,而是没有多个线程轮询队列(这里没有显示,但这是一个非阻塞操作,如果以高频率进行轮询,将导致巨大的交易成本多个地方)。

    // BlockingCollection<>(1) will block if try to add more than 1 job to queue (no
    // point in being greedy!), or is empty on take.
    var BlockingCollection<Job> jobs = new BlockingCollection<Job>(1);
    
    // Setup a number of consumer threads.
    // Determine MAX_CONSUMER_THREADS empirically, if 4 core CPU and 50% of time
    // in job is blocked waiting IO then likely be 8.
    for(int numConsumers = 0; numConsumers < MAX_CONSUMER_THREADS; numConsumers++)
    {
       Thread consumer = new Thread(() =>
       {
          while (!jobs.IsCompleted)
          {
             var job = jobs.Take();
             job.Execute();
          }
       }
       consumer.Start();
    }
    
    // Producer to take items of queue and put in blocking collection ready for processing
    while (true)
    {
        var job = Queue.PopJob();
        if (job != null)
           jobs.Add(job);
        else
        {
           jobs.CompletedAdding()
           // May need to wait for running jobs to finish
           break;
        }
    }
    

6 个答案:

答案 0 :(得分:22)

我刚给了answer,这非常适用于这个问题。

基本上,TPL Task类用于安排CPU绑定工作。它不是用于阻止工作。

您正在使用非CPU资源:等待服务回复。这意味着TPL会错误地管理您的资源,因为它会假定CPU有一定程度的限制。

自己管理资源:启动固定数量的线程或LongRunning任务(基本相同)。根据经验确定线程数。

您不能将不可靠的系统投入生产。出于这个原因,我建议#1但限制。不要创建与工作项一样多的线程。创建尽可能多的线程来使远程服务饱和。给自己写一个帮助函数,它产生N个线程并使用它们来处理M个工作项。通过这种方式,您可以获得完全可预测且可靠的结果。

答案 1 :(得分:12)

await导致的潜在流分裂和延续,稍后在您的代码或第三方库中,将无法很好地处理长时间运行的任务(或线程),所以不要打扰使用长时间运行任务。在async/await世界,它们毫无用处。更多详情here

您可以拨打ThreadPool.SetMaxThreads,但在拨打此电话之前,请确保使用低于或等于最大值的值设置ThreadPool.SetMinThreads的最小线程数。顺便说一句,MSDN文档是错误的。使用这些方法调用可以低于机器上的内核数量,至少在.NET 4.5和4.6中,我使用这种技术来降低内存限制32位服务的处理能力。

但是,如果您不希望限制整个应用程序而只限制它的处理部分,则自定义任务计划程序将完成此任务。很久以前,MS发布了samples几个自定义任务调度程序,包括LimitedConcurrencyLevelTaskScheduler。使用Task.Factory.StartNew手动生成主要处理任务,提供自定义任务调度程序,由它生成的每个其他任务都将使用它,包括async/await甚至Task.Yield,用于在早期实现异步用async方法。

但是对于您的特定情况,两种解决方案都不会在完成工作之前停止用尽您的工作队列。这可能是不可取的,具体取决于您的队列的实现和目的。它们更像是“解雇一堆任务,让调度程序找到执行它们的时间”类型的解决方案。因此,或许更合适的方法可能是通过semaphores更严格地控​​制作业执行的方法。代码如下所示:

semaphore = new SemaphoreSlim(max_concurrent_jobs);

while(...){
 job = Queue.PopJob();
 semaphore.Wait();
 ProcessJobAsync(job);
}

async Task ProcessJobAsync(Job job){
 await Task.Yield();
 ... Process the job here...
 semaphore.Release();
}

皮肤猫的方法不止一种。使用您认为合适的内容。

答案 2 :(得分:8)

Microsoft有一个非常酷的库,名为DataFlow,它可以完全满足您的需求(以及更多)。详情here

您应该使用ActionBlock类并设置ExecutionDataflowBlockOptions对象的MaxDegreeOfParallelism。 ActionBlock可以很好地使用async / await,因此即使等待外部调用,也不会开始处理新的作业。

ExecutionDataflowBlockOptions actionBlockOptions = new ExecutionDataflowBlockOptions
{
     MaxDegreeOfParallelism = 10
};

this.sendToAzureActionBlock = new ActionBlock<List<Item>>(async items => await ProcessItems(items),
            actionBlockOptions);
...
this.sendToAzureActionBlock.Post(itemsToProcess)

答案 3 :(得分:7)

此处的问题似乎并不是太多正在运行 Task,而是计划 Task太多了。无论执行速度有多快,您的代码都会尝试尽可能多地安排TaskLongRunning。如果你的工作太多,这意味着你会得到OOM。

因此,您提出的解决方案都不会真正解决您的问题。如果只是简单地指定Thread来解决您的问题,那么这很可能是因为创建一个新的LongRunning(这是LongRunning所做的)需要一些时间,这有效地限制了新的工作。因此,此解决方案只能偶然发挥作用,并且很可能在以后导致其他问题。

关于解决方案,我主要同意usr:最合适的解决方案是创建固定数量的Queue.PopJob()任务,并有一个调用lock的循环(受{{{{ 1}}如果该方法不是线程安全的)并且Execute()是该作业。

更新:经过一番思考,我意识到以下尝试很可能会表现得非常糟糕。只有在您确信它能够很好地适合您时才使用它。


但是TPL试图找出最佳的并行度,即使对于IO绑定的Task也是如此。因此,您可以尝试使用它来获得优势。长Task s在这里不起作用,因为从TPL的角度来看,似乎没有完成任何工作,它会一遍又一遍地开始新的Task。您可以做的是在每个Task的末尾开始一个新的Task。通过这种方式,TPL将知道发生了什么,并且其算法可能运行良好。另外,要让TPL决定并行度,在其第一行Task的开头,开始另一行Task s。

此算法可能运行良好。但也有可能TPL会对并行度做出错误的决定,我实际上没有尝试过这样的事情。

在代码中,它看起来像这样:

void ProcessJobs(bool isFirst)
{
    var job = Queue.PopJob(); // assumes PopJob() is thread-safe
    if (job == null)
        return;

    if (isFirst)
        Task.Factory.StartNew(() => ProcessJobs(true));

    job.Execute();

    Task.Factory.StartNew(() => ProcessJob(false));
}

开头
Task.Factory.StartNew(() => ProcessJobs(true));

答案 4 :(得分:1)

TaskCreationOptions.LongRunning对于阻止任务很有用,在这里使用它是合法的。它的作用是建议调度程序将一个线程专用于任务。调度程序本身会尝试将线程数保持在与CPU内核数相同的级别上,以避免过多的上下文切换。

Threading in C# by Joseph Albahari

中有详细描述

答案 5 :(得分:1)

我使用消息队列/邮箱机制来实现这一点。它类似于演员模型。我有一个有MailBox的类。我称这个班为我的工人。&#34;它可以接收消息。这些消息排队,它们本质上定义了我希望工作者运行的任务。在出列下一条消息并开始下一个任务之前,工作人员将使用Task.Wait()完成其任务。

通过限制我拥有的工作者数量,我可以限制正在运行的并发线程/任务的数量。

在源代码中,我在分布式计算引擎的博客文章中概述了这一点。如果你看一下IActor和WorkerNode的代码,我希望它有意义。

https://long2know.com/2016/08/creating-a-distributed-computing-engine-with-the-actor-model-and-net-core/