如何正确执行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++
类的常见问题是这一系列事件可能发生:
- 线程A读取
count
的值。 - 线程B读取
count
的值。 - 线程A递增其本地副本。
- 线程B递增其本地副本。
- 线程A将递增的值写回
count
。 - 线程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++;
呼叫。
最好将任何数据库或文件系统操作存储在本地缓冲区变量中,而不是将其锁定。 锁定会降低性能。