14

我正在尝试实现Parallel.ForEach模式并跟踪进度,但我缺少一些关于锁定的东西。以下示例在 时计数为 1000 threadCount = 1,但在threadCount> 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;
         }
      }
   }
}
4

6 回答 6

26

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

  1. 线程 A 读取 的值count
  2. 线程 B 读取 的值count
  3. 线程 A 递增其本地副本。
  4. 线程 B 递增其本地副本。
  5. 线程 A 将增加的值写回count.
  6. 线程 B 将增加的值写回count.

这样,线程 A 写入的值被线程 B 覆盖,所以该值实际上只增加了一次。

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

您需要做的是锁定整个操作,以便在线程 A 递增值时,线程 B 根本无法访问它:

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

如果你知道你只需要递增,你可以创建一个Progress封装这个的方法。

于 2013-01-24T12:48:33.033 回答
25

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

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

于 2015-05-28T16:58:27.177 回答
1

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

lock { ++progress.CurrentCount; }

(我个人更喜欢前增量的外观而不是后增量,因为“++。”这件事在我的脑海中发生了冲突!但后增量当然会起作用。)

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

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

然而,我们生活在这样一个时代,许多开发团队都在教条主义地运作,并使用 StyleCop 之类的工具来强制执行他们的教条主义。这样的工具,不像程序员,不够聪明,无法判断什么时候使用字段是可以接受的,所以“简单到连 StyleCop 都可以检查的规则”总是变成“将字段封装为属性”,“不要使用公共字段”等等...

于 2014-03-25T08:53:50.320 回答
0

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

 object sync = new object();
        Parallel.ForEach(ids, new ParallelOptions {MaxDegreeOfParallelism = threadCount},
                         id =>
                             {
                                 lock(sync)
                                 progress.CurrentCount++;
                             });
于 2013-01-24T12:27:13.960 回答
0

这里的问题++不是原子的——一个线程可以在另一个线程读取值和存储(现在不正确的)递增值之间读取和递增值。这可能是因为有一个属性包裹着你的int.

例如

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

setter 和 getter 周围的锁对此无济于事,因为没有什么可以阻止它们的lock块被乱序调用。

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

相反,您可以公开_lock并让lock块在progress.CurrentCount++;调用周围。

于 2013-01-24T12:48:03.270 回答
0

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

于 2017-01-23T10:32:43.393 回答