【问题标题】:Parallel.ForEach vs Task.Factory.StartNewParallel.ForEach 与 Task.Factory.StartNew
【发布时间】:2011-06-27 21:47:11
【问题描述】:

下面的代码sn-ps有什么区别?两者都不会使用线程池线程吗?

例如,如果我想为集合中的每个项目调用一个函数,

Parallel.ForEach<Item>(items, item => DoSomething(item));

vs

foreach(var item in items)
{
  Task.Factory.StartNew(() => DoSomething(item));
}

【问题讨论】:

    标签: c# c#-4.0 task-parallel-library parallel-extensions


    【解决方案1】:

    第一个是更好的选择。

    Parallel.ForEach 在内部使用Partitioner&lt;T&gt; 将您的集合分发到工作项中。它不会对每个项目执行一项任务,而是将其批处理以降低所涉及的开销。

    第二个选项将为您的收藏中的每个项目安排一个Task。虽然结果(几乎)相同,但这会带来比必要更多的开销,尤其是对于大型集合,并导致整体运行时间变慢。

    仅供参考 - 如果需要,可以使用适当的 overloads to Parallel.ForEach 来控制所使用的分区器。有关详细信息,请参阅 MSDN 上的Custom Partitioners

    在运行时,主要区别在于第二个将异步执行。这可以使用 Parallel.ForEach 复制:

    Task.Factory.StartNew( () => Parallel.ForEach<Item>(items, item => DoSomething(item)));
    

    通过这样做,您仍然可以利用分区器,但在操作完成之前不要阻塞。

    【讨论】:

    • IIRC,由 Parallel.ForEach 完成的默认分区也考虑了可用的硬件线程数,从而使您不必计算最佳的任务数来启动。查看微软的Patterns of Parallel Programming 文章;它对所有这些东西都有很好的解释。
    • @Mal:有点……这实际上不是分区器,而是任务调度器的工作。默认情况下,TaskScheduler 使用新的 ThreadPool,它现在可以很好地处理这个问题。
    • 谢谢。我知道我应该离开“我不是专家,但是……”警告。 :)
    • @ReedCopsey:如何将通过 Parallel.ForEach 启动的任务附加到包装任务?这样当您在包装任务上调用 .Wait() 时,它会挂起,直到并行运行的任务完成?
    • @Tarkus 如果您发出多个请求,最好在每个工作项(在您的并行循环中)中使用 HttpClient.GetString。没有理由在已经并发的循环中放置异步选项,通常...
    【解决方案2】:

    我做了一个小实验,用“Parallel.For”和“Task”对象运行方法“1,000,000,000(十亿)次。

    我测量了处理器时间,发现并行效率更高。 Parallel.For 将您的任务划分为小工作项,并以最佳方式在所有内核上并行执行它们。在创建大量任务对象时(仅供参考,TPL 将在内部使用线程池)将移动每个任务上的每次执行,从而在框中产生更大的压力,这从下面的实验中可以看出。

    我还制作了一个小视频,它解释了基本的 TPL,并演示了 Parallel.For 如何比普通任务和线程更有效地利用您的核心http://www.youtube.com/watch?v=No7QqSc5cl8

    实验 1

    Parallel.For(0, 1000000000, x => Method1());
    

    实验 2

    for (int i = 0; i < 1000000000; i++)
    {
        Task o = new Task(Method1);
        o.Start();
    }
    

    【讨论】:

    • 这样会更有效率,而且创建线程代价高昂的原因是实验 2 是一个非常糟糕的做法。
    • @Georgi-it 请多多谈论坏事。
    • 对不起,我的错误,我应该澄清一下。我的意思是循环创建任务到 1000000000。开销是不可想象的。更何况 Parallel 一次不能创建超过 63 个任务,这使得它在这种情况下更加优化。
    • 这适用于 1000000000 个任务。但是,当我处理图像(重复,缩放分形)并执行 Parallel.For 在线时,许多内核在等待最后一个线程完成时处于空闲状态。为了让它更快,我自己将数据细分为 64 个工作包并为其创建任务。 (然后 Task.WaitAll 等待完成。)这个想法是让空闲线程拿起一个工作包来帮助完成工作,而不是等待 1-2 个线程完成他们(Parallel.For)分配的块。跨度>
    • Mehthod1() 在这个例子中做了什么?
    【解决方案3】:

    Parallel.ForEach 将优化(甚至可能不启动新线程)并阻塞,直到循环完成,并且 Task.Factory 将为每个项目显式创建一个新任务实例,并在它们完成之前返回(异步任务)。 Parallel.Foreach 效率更高。

    【讨论】:

      【解决方案4】:

      在我看来,最现实的情况是任务需要完成繁重的操作。 Shivprasad 的方法更多地关注对象创建/内存分配,而不是计算本身。我做了一项研究,调用以下方法:

      public static double SumRootN(int root)
      {
          double result = 0;
          for (int i = 1; i < 10000000; i++)
              {
                  result += Math.Exp(Math.Log(i) / root);
              }
              return result; 
      }
      

      此方法的执行大约需要 0.5 秒。

      我使用 Parallel 调用了 200 次:

      Parallel.For(0, 200, (int i) =>
      {
          SumRootN(10);
      });
      

      然后我用老式的方式调用了 200 次:

      List<Task> tasks = new List<Task>() ;
      for (int i = 0; i < loopCounter; i++)
      {
          Task t = new Task(() => SumRootN(10));
          t.Start();
          tasks.Add(t);
      }
      
      Task.WaitAll(tasks.ToArray()); 
      

      第一个案例在 26656 毫秒内完成,第二个案例在 24478 毫秒内完成。我重复了很多次。每次第二种方法都快一点。

      【讨论】:

      • 使用 Parallel.For 是老式的方式。对于不统一的工作单元,建议使用 Task。微软 MVP 和 TPL 的设计者还提到,使用任务将更有效地使用线程,即在等待其他单元完成时不会阻塞那么多。
      猜你喜欢
      • 2012-06-01
      • 2016-04-22
      • 2012-03-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-31
      • 2012-08-12
      相关资源
      最近更新 更多