2016-12-30 132 views
3

我正在看一个页面上的例子。斯蒂芬·克利里的书40,为什么这里需要一个锁?

// Note: this is not the most efficient implementation. 
// This is just an example of using a lock to protect shared state. 
static int ParallelSum(IEnumerable<int> values) 
{ 
    object mutex = new object(); 
    int result = 0; 
    Parallel.ForEach(source: values, 
     localInit:() => 0, 
     body: (item, state, localValue) => localValue + item, 
     localFinally: localValue => 
     { 
      lock (mutex) 
       result += localValue; 
     }); 
    return result; 
} 

和我有点困惑,为什么需要对lock。因为如果我们所做的只是汇总了一组int s,比如说{1, 5, 6},那么我们不需要关心以任何顺序递增的共享总和result

(1 + 5) + 6 = 1 + (5 + 6) = (1 + 6) + 5 = ... 

有人可以解释我的思维在哪里有缺陷吗?

我猜我有点困惑该方法的身体不能简单地

int result = 0; 
Parallel.ForReach(values, (val) => { result += val; }); 
return result; 

回答

2

声明result += localValue;确实在说result = result + localValue;您正在阅读和更新由不同线程共享的资源(变量)。这很容易导致竞赛状况。 lock确保在任何给定时刻这个语句被一个线程访问。

10

运算,如加法不原子,因此没有线程安全的。在示例代码中,如果该锁被省略,则完全有可能两个附加操作几乎同时执行,导致其中一个值被覆盖或错误地添加。有一种方法线程安全地增加整数:Interlocked.Add(int, int)。但是,由于没有被使用,所以在示例代码中需要使用锁来确保一次完成最多一次非原子加法操作(按顺序而不是并行)。

+0

我认为它的赋值操作符导致了这个问题。不是增加它自己。 –

+6

@ M.kazemAkhgary也不完全正确。赋值和加法都是独立的原子。什么*不*原子是增量,它由一个读,一个添加,然后一个赋值组成。每三个操作都是100%原子,但所有三个操作都不是原子的。 – Servy

7

它不是在“结果”被更新的顺序,其更新它,记住,运营商的race condition+=不是原子,所以两个线程可能不会看到其他的线程的UDPATE他们触摸它

+0

对我来说这似乎很奇怪,CPU甚至会让这种碰撞发生。当我编写C#时,我从不觉得必须考虑可能的硬件级错误。我想我应该是。 – user7127000

+0

请做!它与C或C++没什么区别 –

+2

@ user7127000它不是*错误*,它是一个基本特征。如果CPU不允许重新排序操作,那么你的计算机将比现在慢几百倍。这些速度的许多数量级的代价是线程之间的共享状态非常难以处理,所以你应该不惜一切代价避免这样做,包括在你的例子中,因为你的代码会很多如果在单个线程上运行,而不是几个,运行速度会更快。 – Servy