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

C# 限制通过并行任务库运行的活动任务数的最佳方法,c#,.net,task-parallel-library,C#,.net,Task Parallel Library,考虑一个队列,该队列包含大量需要处理的作业。队列的限制是一次只能得到一个作业,并且无法知道有多少个作业。这些作业需要10秒才能完成,并且需要大量等待web服务的响应,因此不会受到CPU的限制 如果我用这样的东西 while (true) { var job = Queue.PopJob(); if (job == null) break; Task.Factory.StartNew(job.Execute); } 然后它会从队列中疯狂地弹出作业,比它完成作业的速

考虑一个队列,该队列包含大量需要处理的作业。队列的限制是一次只能得到一个作业,并且无法知道有多少个作业。这些作业需要10秒才能完成,并且需要大量等待web服务的响应,因此不会受到CPU的限制

如果我用这样的东西

while (true)
{
   var job = Queue.PopJob();
   if (job == null)
      break;
   Task.Factory.StartNew(job.Execute); 
}
然后它会从队列中疯狂地弹出作业,比它完成作业的速度快得多,内存耗尽,然后落在它的屁股上。>。我刚才给出了一个非常适用于这个问题的答案

基本上,TPL任务类是用来安排CPU工作的。它不是为阻塞工作而制造的

您使用的资源不是CPU:正在等待服务回复。这意味着TPL将错开您的资源,因为它在一定程度上假定CPU有界

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


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

这里的问题似乎不是太多正在运行的
任务,而是太多计划的
任务。您的代码将尝试安排尽可能多的
任务
s,无论它们执行得有多快。如果你有太多的工作,这意味着你会得到很多

因此,您提出的任何解决方案都不能真正解决您的问题。如果简单地指定
LongRunning
似乎就能解决问题,那么这很可能是因为创建一个新的
线程
(这就是
LongRunning
所做的)需要一些时间,这有效地限制了获得新作业。因此,这种解决方案只能在偶然情况下起作用,并且很可能在以后导致其他问题

关于解决方案,我基本上同意usr:最简单的解决方案是创建固定数量的
长时间运行的
任务,并有一个循环来调用
队列.PopJob()
(如果该方法不是线程安全的,则由
锁保护)和
执行()
作业

更新:经过进一步思考,我意识到以下尝试很可能会表现得很糟糕。只有当你确信它会对你有好处时才使用它


但TPL试图找出最佳并行度,即使是IO绑定的
任务
s。所以,你可以试着利用这个优势。长
任务
s在这里不起作用,因为从第三方物流的角度来看,似乎没有完成任何工作,它会一次又一次地开始新的
任务。相反,您可以在每个
任务
结束时启动一个新的
任务
。通过这种方式,TPL将知道发生了什么,其算法可能运行良好。此外,为了让TPL决定并行度,在第一行的
任务开始时,启动另一行
任务

这个算法可能运行良好。但也有可能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));
}
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();
}
首先是

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

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


微软有一个非常酷的名为DataFlow的库,它可以完全满足您的需要(还有更多)。细节

您应该使用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)
ExecutionDataflowBlockOptions actionBlockOptions=新的ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism=10
};
this.sendToAzureActionBlock=新操作块(异步项=>等待处理项(项),
行动方案);
...
this.sendToAzureActionBlock.Post(itemsToProcess)

wait
引起的潜在流拆分和延续,稍后在您的代码或第三方库中,将无法很好地处理长时间运行的任务(或线程),因此不要麻烦使用长时间运行的任务。在
async/await
世界中,它们是无用的。更多细节

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


但是,如果您不希望限制整个应用程序,而只限制其处理部分,则自定义任务调度器将执行此任务。很久以前,MS发布了几个自定义任务调度器,包括
limitedconcurrenceyleveltaskscheduler
。使用
task.Factory.StartNew
(提供自定义任务调度程序)手动生成主处理任务,它生成的所有其他任务都将使用它,包括
async/wait
甚至
task.Yield
,用于在
async
方法中尽早实现异步
ExecutionDataflowBlockOptions actionBlockOptions = new ExecutionDataflowBlockOptions
{
     MaxDegreeOfParallelism = 10
};

this.sendToAzureActionBlock = new ActionBlock<List<Item>>(async items => await ProcessItems(items),
            actionBlockOptions);
...
this.sendToAzureActionBlock.Post(itemsToProcess)
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();
}