我正在尝试实现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;
}
}
}
}
答案 0 :(得分:19)
从多个线程(共享count++
变量)调用count
之类的东西的常见问题是这一系列事件可能发生:
count
。count
。count
。count
。这样,线程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)
最好将任何数据库或文件系统操作存储在本地缓冲区变量中,而不是将其锁定。锁定降低了性能。