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

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

 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; } } } } 

从多个线程(共享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上创建一个封装此方法的方法。

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

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

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

 lock { ++progress.CurrentCount; } 

(我个人更喜欢在增量后增加前体的外观,因为“++。”的东西在我的脑海中发生了冲突!但后增量当然会起作用。)

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

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

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

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

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

这里的问题是++不是primefaces的 – 一个线程可以读取并递增读取该值的另一​​个线程之间的值,并且它存储(现在不正确的)递增值。 事实上,包含你的int的属性可能更加复杂。

例如

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

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

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

相反,你可以公开_lock并让lock块围绕progress.CurrentCount++; 呼叫。

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