将相同的值写入相同的内存位置会导致数据竞争吗?

时间:2011-11-29 18:38:18

标签: c++ multithreading memory synchronization race-condition

考虑以下代码,将相同的值写入多个线程的相同内存位置:

void f(int* buf, int n, int* p) {
    for(int i = 0; i < n; i++)
        buf[i] = i;
    *p = buf[n/2];
}

void g(int* buf, int n) {
    int x1, x2;
    thread t1(f, buf, n, &x1);
    thread t2(f, buf, n, &x2);
    t1.join();
    t2.join();
    assert(x1 == x2);
}

虽然它很有意思,但我不太关心标准给出的保证,因为我猜它没有给出。我真正关心的是上述代码在真实世界多处理器硬件上的行为。 assert总是会通过,还是有任何竞争条件,缓存同步问题等等。?

3 个答案:

答案 0 :(得分:8)

有一场比赛,但在你的例子中,两个线程都会将相同的值写入相同的地址。由于您没有进行任何读取 - 修改 - 写入,而只是编写预定的数字,因此在大多数情况下这是安全的。编写int将是大多数系统上的原子指令。例外情况是,如果您在使用一系列指令存储int的8位微处理器上运行此代码。在这种情况下,它也可能仍然有效,但取决于执行多字节存储的库代码的实现。

答案 1 :(得分:6)

当一个线程写入的效果可由另一个线程观察时,关于多线程问题的内存模型。在您发布的代码中,两个线程都将相同的值写入相同的内存位置,因此无论哪个线程的写buf[n/2]读取都无关紧要。

现代处理器采用缓存一致性协议,例如MESI,因此当线程同时写入缓冲区时,CPU之间会发送大量消息来同步缓存行的缓存行比非并发场景(false sharing effect)运行得慢得多。

这里写入是否为原子并不重要,因为两个线程都将相同的值写入相同的内存位置。有一场比赛,但哪个线程获胜并不重要,因为即使部分写入,观察到的值也会相同。

答案 2 :(得分:2)

好的,这里的关键点确实是,正如@Maxim所说,缓存一致性。在使用缓存一致性的机器中,它确实是不可能的。

但是,可能在没有缓存一致性的计算机上出错。我不知道具体的架构,虽然它们因自然选择而几乎绝迹,但据我所知还有一些剩余。 (如果您知道一个例子,请发表评论。)

这是一个表,表示两个线程的执行,用内存填充内存中的归零区域。为简洁起见,该示例按比例缩小32倍,即这里的每个数字代表所讨论的4字节int。缓存行大小为4 ints == 4位数。标记为“刷新”的行是将片上高速缓存刷新到主存储器的点。实际上它是非确定性的,因为它可能在任何时候发生,例如,由于先发制人的任务切换。

Core 1 cache              Memory                    Core 2 cache
------------------------------------------------------------------------------
                          0000
0000 (load cache)         0000
1000 (set 1st bit)        0000
1100 (set 2nd bit)        0000                      0000 (load cache)
**** (flush)              1100
                          1100                      1000 (set 1st bit)
                          1000                      **** (flush)
                          1000                      1000 (load cache)
                          1000                      1100 (set 2nd bit)
1000 (load cache)         1000                      1110 (set 3rd bit)
1010 (set 3rd bit)        1000                      1111 (set 4th bit)
1011 (set 4th bit)        1111                      **** (flush)
**** (flush)              1011

所以我们最终得到了错误的结果。

我再次强调,此反例只适用于缓存不连贯的机器