如何正确执行Parallel.ForEach,锁定和进度报告

时间:2013-01-24 11:51:06

标签: c# locking task-parallel-library

我正在尝试实现Parallel.ForEach模式并跟踪进度,但我遗漏了一些关于锁定的内容。以下示例在threadCount = 1时计为1000,而在threadCount>时计为1000。 1.这样做的正确方法是什么?

class Program
{
   static void Main()
   {
      var progress = new Progress();
      var ids = Enumerable.Range(1, 10000);
      var threadCount = 2;

      Parallel.ForEach(ids, new ParallelOptions { MaxDegreeOfParallelism = threadCount }, id => { progress.CurrentCount++; });

      Console.WriteLine("Threads: {0}, Count: {1}", threadCount, progress.CurrentCount);
      Console.ReadKey();
   }
}

internal class Progress
{
   private Object _lock = new Object();
   private int _currentCount;
   public int CurrentCount
   {
      get
      {
         lock (_lock)
         {
            return _currentCount;
         }
      }
      set
      {
         lock (_lock)
         {
            _currentCount = value;
         }
      }
   }
}

6 个答案:

答案 0 :(得分:19)

从多个线程(共享count++变量)调用count之类的东西的常见问题是这一系列事件可能发生:

  1. 线程A读取count
  2. 的值
  3. 线程B读取count
  4. 的值
  5. 线程A递增其本地副本。
  6. 线程B递增其本地副本。
  7. 线程A将递增的值写回count
  8. 线程B将递增的值写回count
  9. 这样,线程A写入的值被线程B覆盖,因此该值实际上只增加一次。

    您的代码会在操作1,2(get)和5,6(set)周围添加锁定,但这不会阻止有问题的事件序列。

    你需要做的是锁定整个操作,这样当线程A递增值时,线程B根本无法访问它:

    lock (progressLock)
    {
        progress.CurrentCount++;
    }
    

    如果您知道只需要递增,则可以在Progress上创建一个封装此方法的方法。

答案 1 :(得分:18)

老问题,但我认为有更好的答案。

您可以使用Interlocked.Increment(ref progress)报告进度,这样您就不必担心将写操作锁定为进度。

答案 2 :(得分:1)

最简单的解决方案实际上是用字段替换属性,

lock { ++progress.CurrentCount; }

(我个人更喜欢看到前增量的前增量,因为" ++。"事情在我脑海中发生了冲突!但后增量当然会有相同的效果。)

这将带来额外的好处,即减少开销和争用,因为更新字段比调用更新字段的方法更快。

当然,将其封装为属性也具有优势。 IMO,因为字段和属性语法是相同的,当属性自动实现或等效时,在字段上使用属性的唯一好处是,当您有一个场景,您可能希望部署一个程序集而不必构建和部署依赖项重新组装。否则,您也可以使用更快的字段!如果需要检查值或添加副作用,您只需将字段转换为属性并再次构建。因此,在许多实际情况中,使用场不会受到惩罚。

然而,我们生活在一个许多开发团队教条操作的时代,并使用StyleCop等工具来强化他们的教条主义。与编码器不同,这些工具不够聪明,无法判断何时使用某个字段是可以接受的,因此总是足够简单的规则,即使是StyleCop也可以检查"变为"将字段封装为属性","不使用公共字段"等等......

答案 3 :(得分:0)

从属性中删除锁定语句并修改主体:

 object sync = new object();
        Parallel.ForEach(ids, new ParallelOptions {MaxDegreeOfParallelism = threadCount},
                         id =>
                             {
                                 lock(sync)
                                 progress.CurrentCount++;
                             });

答案 4 :(得分:0)

这里的问题是++不是原子的 - 一个线程可以读取并递增读取该值的另一​​个线程与存储(现在不正确)递增值之间的值。事实上,int包含了一个属性。

e.g。

Thread 1        Thread 2
reads 5         .
.               reads 5
.               writes 6
writes 6!       .

setter和getter周围的锁对此没有帮助,因为没有什么可以阻止lock阻止他们被调用乱序。

通常情况下,我建议您使用Interlocked.Increment,但不能将其与属性一起使用。

相反,您可以公开_lock并让lock阻止在progress.CurrentCount++;来电。

答案 5 :(得分:0)

最好将任何数据库或文件系统操作存储在本地缓冲区变量中,而不是将其锁定。锁定降低了性能。