考虑一个队列,其中包含需要处理的批次作业。队列限制一次只能获得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个替代品
用
替换Task.Factory.StartNewTask task = new Task(job.Execute,TaskCreationOptions.LongRunning)
task.Start();
这似乎在某种程度上解决了问题,但我不是clear exactly what this is doing,如果这是最好的方法。
创建custom task scheduler that limits the degree of concurrency
使用BlockingCollection之类的内容在开始时将作业添加到集合中,并在完成后删除以限制可以运行的数字。
#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;
}
}
答案 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
太多了。无论执行速度有多快,您的代码都会尝试尽可能多地安排Task
个LongRunning
。如果你的工作太多,这意味着你会得到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内核数相同的级别上,以避免过多的上下文切换。
答案 5 :(得分:1)
我使用消息队列/邮箱机制来实现这一点。它类似于演员模型。我有一个有MailBox的类。我称这个班为我的工人。&#34;它可以接收消息。这些消息排队,它们本质上定义了我希望工作者运行的任务。在出列下一条消息并开始下一个任务之前,工作人员将使用Task.Wait()完成其任务。
通过限制我拥有的工作者数量,我可以限制正在运行的并发线程/任务的数量。
在源代码中,我在分布式计算引擎的博客文章中概述了这一点。如果你看一下IActor和WorkerNode的代码,我希望它有意义。