我正在使用C#TPL而我遇到了生产者/消费者代码的问题...出于某种原因,TPL不会重复使用线程并不断创建新的线程
我做了一个简单的例子来证明这种行为:
class Program
{
static BlockingCollection<int> m_Buffer = new BlockingCollection<int>(1);
static CancellationTokenSource m_Cts = new CancellationTokenSource();
static void Producer()
{
try
{
while (!m_Cts.IsCancellationRequested)
{
Console.WriteLine("Enqueuing job");
m_Buffer.Add(0);
Thread.Sleep(1000);
}
}
finally
{
m_Buffer.CompleteAdding();
}
}
static void Consumer()
{
Parallel.ForEach(m_Buffer.GetConsumingEnumerable(), Run);
}
static void Run(int i)
{
Console.WriteLine
("Job Processed\tThread: {0}\tProcess Thread Count: {1}",
Thread.CurrentThread.ManagedThreadId,
Process.GetCurrentProcess().Threads.Count);
}
static void Main(string[] args)
{
Task producer = new Task(Producer);
Task consumer = new Task(Consumer);
producer.Start();
consumer.Start();
Console.ReadKey();
m_Cts.Cancel();
Task.WaitAll(producer, consumer);
}
}
此代码创建了2个任务,即producer和consumer。 Produces每秒添加1个工作项,而Consumer只打印出包含信息的字符串。我认为在这种情况下1个消费者线程就足够了,因为任务的处理速度比添加到队列中的速度要快得多,但实际发生的是,进程中每隔一个线程数增加1 ...就好像TPL正在为每个项目创建新线程
在尝试了解发生了什么之后我还注意到了另一件事:即使BlockingCollection大小为1,但是在一段时间后,Consumer开始以突发方式进行调用,例如,这就是它的开始:
排队职位
作业处理线程:4处理线程数:9
排队职位
作业处理线程:6处理线程数:9
排队职位
作业处理线程:5处理线程数:10
排队职位
作业处理线程:4处理线程数:10
排队职位
作业处理线程:6处理线程数:11
这就是它在不到一分钟之后处理项目的方式:
排队职位
作业处理线程:25处理线程数:52
排队职位
排队职位
作业处理线程:5处理线程数:54
作业处理线程:5处理线程数:54
并且因为线程在完成Parallel.ForEach循环之后被释放(我在这个例子中没有显示它,但是它在真实的项目中)我假设它与ForEach具体有关...我发现了这个artice http://reedcopsey.com/2010/01/26/parallelism-in-net-part-5-partitioning-of-work/,我认为我的问题是由这个默认分区程序引起的,所以我从TPL示例中获取了自定义分区程序,它逐个地提供了Consumer threads项目,虽然它修复了执行顺序(摆脱了延迟)...
排队职位
作业处理线程:71处理线程数:140
排队职位
作业处理线程:12处理线程数:141
排队职位
作业处理线程:72处理线程数:142
排队职位
作业处理线程:38处理线程数:143
排队职位
作业处理线程:73处理线程数:143
排队职位
作业处理线程:21处理线程数:144
排队职位
作业处理线程:74处理线程数:145
......它没有阻止线程的增长
我了解ParallelOptions.MaxDegreeOfParallelism,但我仍然想了解TPL发生了什么,以及为什么它无缘无故地创建了数百个线程
在我的项目中,我需要运行几个小时并从数据库中读取新数据的代码,将其放入BlockingCollections并由其他代码处理数据,每5秒就有1个新项目,需要几毫秒几乎一分钟来处理它,并在运行大约10分钟后,线程数超过了1000个线程
答案 0 :(得分:6)
有两件事共同导致这种行为:
ThreadPool
尝试为您的情况使用最佳线程数。但是如果池中的某个线程阻塞,则池会将此视为该线程没有执行任何有用的工作,因此很快就会创建另一个线程。这意味着如果你有很多阻塞,ThreadPool
在猜测最佳线程数时非常糟糕,并且它往往会创建新线程,直到达到极限。
Parallel.ForEach()
信任ThreadPool
猜测正确的线程数,除非您明确设置最大线程数。 Parallel.ForEach()
也主要用于有界集合,而不是数据流。
当您将这两件事与GetConsumingEnumerable()
结合使用时,您得到的是Parallel.ForEach()
创建几乎总是被阻止的线程。 ThreadPool
看到了这一点,并且,为了保持CPU的利用率,创建了越来越多的线程。
此处的正确解决方案是设置MaxDegreeOfParallelism
。如果您的计算受CPU约束,则最佳值很可能是Environment.ProcessorCount
。如果它们是IO绑定的,您将必须通过实验找出最佳值。
如果可以使用.Net 4.5,另一个选择是使用TPL Dataflow。这个库专门用于处理数据流,就像你一样,所以它没有你的代码所带来的问题。它实际上甚至比那更好,并且当它当前没有处理任何东西时根本不使用任何线程。
注意:还有一个很好的理由为什么是为每个新项目创建一个新线程,但解释这将需要我更详细地解释Parallel.ForEach()
如何工作,我觉得这里没有必要。