Parallel.For vs常规线程

时间:2012-10-25 14:02:11

标签: c# multithreading performance

我试图理解为什么在以下场景中Parallel.For能够胜过许多线程:考虑一批可以并行处理的作业。在处理这些作业时,可能会添加新工作,然后也需要对其进行处理。 Parallel.For解决方案如下所示:

var jobs = new List<Job> { firstJob };
int startIdx = 0, endIdx = jobs.Count;
while (startIdx < endIdx) {
  Parallel.For(startIdx, endIdx, i => WorkJob(jobs[i]));
  startIdx = endIdx; endIdx = jobs.Count;
}

这意味着Parallel.For需要同步多次。考虑面包优先图算法算法;同步的数量会非常大。浪费时间,没有?

在老式的线程方法中尝试相同的方法:

var queue = new ConcurrentQueue<Job> { firstJob };
var threads = new List<Thread>();
var waitHandle = new AutoResetEvent(false);
int numBusy = 0;
for (int i = 0; i < maxThreads; i++) 
  threads.Add(new Thread(new ThreadStart(delegate {
    while (!queue.IsEmpty || numBusy > 0) {
      if (queue.IsEmpty)
        // numbusy > 0 implies more data may arrive
        waitHandle.WaitOne();

      Job job;
      if (queue.TryDequeue(out job)) {
        Interlocked.Increment(ref numBusy);
        WorkJob(job); // WorkJob does a waitHandle.Set() when more work was found
        Interlocked.Decrement(ref numBusy);
      }
    }
    // others are possibly waiting for us to enable more work which won't happen
    waitHandle.Set(); 
})));
threads.ForEach(t => t.Start());
threads.ForEach(t => t.Join());

Parallel.For代码当然更清晰,但我无法理解,它甚至更快!任务调度程序是那么好吗?同步被激活,没有繁忙的等待,但线程方法一直较慢(对我而言)。这是怎么回事?线程化方法能否更快?

编辑:感谢所有答案,我希望我可以选择多个答案。我选择了那个也显示出实际可能改进的那个。

3 个答案:

答案 0 :(得分:12)

这两个代码示例并不完全相同。

Parallel.ForEach()将使用有限数量的线程并重新使用它们。必须创建多个线程,第二个样本已经开始落后了。这需要时间。

maxThreads的价值是多少?非常关键,在Parallel.ForEach()它是动态的。

  

任务调度程序是否那么好?

非常好。 TPL使用工作窃取和其他自适应技术。你将很难做得更好。

答案 1 :(得分:3)

Parallel.For实际上并没有将项目分解为单个工作单元。它根据计划使用的线程数和要执行的迭代次数分解所有工作(早期)。然后让每个线程同步处理该批处理(可能使用工作窃取或保存一些额外的项目以在末尾附近进行负载平衡)。通过使用这种方法,工作线程实际上永远不会相互等待,而由于您在每次迭代之前/之后使用的大量同步,您的线程始终在彼此等待。

最重要的是,因为它正在使用线程池线程所需的许多线程很可能已经创建,这是另一个有利的优势。

至于同步,Parallel.For的整个要点是所有迭代都可以并行完成,因此几乎不需要进行任何同步(至少在他们的代码中)。

然后当然存在线程数量的问题。线程池有很多非常好的算法和启发式方法,可以根据当前硬件,来自其他应用程序的负载等,帮助它确定在那个时刻需要的线程数。有可能你使用太多或没有足够的线程。

此外,由于您在开始之前不知道您拥有的项目数量,因此我建议使用Parallel.ForEach而不是几个Parallel.For循环。它只是针对您所处的情况而设计的,因此它的启发式将更好地应用。 (这也使代码更清晰。)

BlockingCollection<Job> queue = new BlockingCollection<Job>();

//add jobs to queue, possibly in another thread
//call queue.CompleteAdding() when there are no more jobs to run

Parallel.ForEach(queue.GetConsumingEnumerable(),
    job => job.DoWork());

答案 2 :(得分:1)

你创建了一堆新线程,而Parallel.For正在使用Threadpool。如果你正在使用C#threadpool,你会看到更好的性能,但实际上没有必要这样做。

我会回避推出自己的解决方案;如果有需要自定义的边角情况,请使用TPL并自定义..