c++中良性竞争条件的保证

Guarantees of benign race conditions in C++

本文关键字:条件 竞争 c++      更新时间:2023-10-16

我知道c++标准不保证存在数据竞争(我相信数据竞争有未定义的行为,意味着任何事情都会发生,包括程序终止,修改随机内存等)。

是否有任何架构,其中的数据竞争,包括一个线程写入内存位置和一个线程从同一位置读取(没有同步)不导致读取操作读取一个未定义的值,其中的内存位置是"最终"(内存屏障后)更新到由写操作写入的值?

数据竞争的问题不在于您可以在机器级别读取错误的值。数据竞争的问题是,编译器和处理器都对代码执行大量优化。为了确保这些优化在存在多个线程的情况下是正确的,它们需要关于可以在线程之间共享的变量的额外信息。这样的优化可以例如:

  • 重新排序操作
  • 添加额外的加载和存储操作
  • 删除加载和存储操作

Hans Boehm有一篇很好的论文良性数据竞争,叫做如何用"良性"数据竞争错编译程序。以下节选自本文:

延迟初始化的双重检查

众所周知,这在源代码中是不正确的的水平。一个典型的用例看起来像

if (!init_flag) {
    lock();
    if (!init_flag) {
        my_data = ...;
        init_flag = true;
    }
    unlock();
}
tmp = my_data;

没有什么可以阻止优化编译器重新排序my_datainit_flag的负载,甚至my_data的负载提前在init_flag的第一次测试之前,在init_flag条件下重新加载它没有设置。一些非x86硬件可以执行类似的重新排序,即使编译器不执行任何转换。这两种方法都可能导致my_data的最终读取查看未初始化的值并产生不正确的结果。


下面是另一个例子,int x是一个共享变量,int r是一个局部变量。

int r = x;
if (r == 0)
    printf("foon");
if (r != 0)
    printf("barn");

如果我们只说,读取x导致一个未定义的值,那么程序将输出"foo"或"bar"。但是,如果编译器按如下方式转换代码,程序也可能同时打印两个字符串,或者不打印任何字符串。

if (x == 0)
    printf("foon");
if (x != 0)
    printf("barn");

你可以使用linux操作系统,在c++中,你可以在父进程上分支2个或更多的子进程,你可以让两个进程访问一个内存位置,通过使用同步,你可以实现你想要做的事情。如何在进程fork()之间共享内存?, http://en.wikipedia.org/wiki/Dekker's_algorithm, http://en.wikipedia.org/wiki/Readers%E2%80%93writers_problem,

总是导致竞争位置的一个例子:要求两个线程向相同的变量写入不同的值。让我们假设

  • 线程1设置变量a为1
  • 线程2设置变量a为2

你会得到竞争条件,即使有互斥锁,因为

  • 如果线程1先执行,那么你得到a=1然后a=2。
  • 如果线程2先执行,那么你得到a=2然后a=1。

线程的顺序取决于操作系统,不能保证哪个线程会先执行。否则,它将是顺序的,不需要在单独的线程中执行。

假设现在根本没有同步,并且在第一个线程中执行a=a+1,在第二个线程中执行a=a+2。a的初值为0

在汇编中,生成的代码是将a的值复制到一个寄存器中,加1(对于第一个线程,加2)。

如果根本没有同步,则可以有以下顺序,例如

  • Thread1:拷贝到reg1的值。reg1包含0

  • Thread2:一个拷贝到reg2的值。reg2包含0

  • Thread1: reg1的值增加1。现在包含1

  • Thread2: reg2添加的值现在包含2

  • Thread1: reg1的值增加1。现在包含1

  • Thread2: reg2添加的值现在包含2

  • Thread1: reg1的值放到a.现在a包含1

  • Thread2: reg2的值放到a.现在a包含2

如果你已经执行了thread1,那么顺序执行线程2,你将在最后得到a=3。

现在假设a是一个指针,也就是一个地址,所以你知道,得到一个错误的指针地址可能会导致程序崩溃。所以错误的同步会导致程序崩溃

有道理吗?