序列点是否会阻止代码在关键部分边界重新排序?

Pee*_*oot 17 c++ concurrency multithreading locking

假设有一个基于锁的代码,如下所示,其中互斥体用于防止不适当的并发读写

mutex.get() ; // get a lock.

T localVar = pSharedMem->v ; // read something
pSharedMem->w = blah ; // write something.
pSharedMem->z++ ;      // read and write something.

mutex.release() ; // release the lock.
Run Code Online (Sandbox Code Playgroud)

如果假设生成的代码是按程序顺序创建的,则仍然需要适当的硬件内存屏障,如isync,lwsync,.acq,.rel.我假设这个问题是互斥实现负责这个部分,提供一个保证pSharedMem读取和写入都发生在"get"之后,"之前"发布()[但周围的读写可以进入关键部分,因为我期望是互斥实现的标准].我还假设在适当的情况下在互斥体实现中使用volatile访问,但是volatile不用于受互斥体保护的数据(理解为什么volatile似乎不是受互斥保护的数据的必要数据实际上是这个问题).

我想了解是什么阻止编译器移动关键区域之外的pSharedMem访问.在C和C++标准中,我看到有一个序列点的概念.我发现标准文档中的大部分序列点文本都是不可理解的,但是如果我要猜测它是什么,那么声明代码不应该在有未知副作用的调用的点上重新排序.这是它的主旨吗?如果是这种情况,编译器在这里有什么样的优化自由度?

随着编译器进行棘手的优化,例如配置文件驱动的过程间内联(甚至跨文件边界),甚至未知副作用的概念也会变得模糊.

在这里以自包含的方式解释这个问题可能超出了一个简单问题的范围,因此我很容易被指向引用(最好是在线并且针对凡人程序员而不是编译器编写者和语言设计者).

编辑:(回应Jalf的回复)

我提到了像lwsync和isync这样的内存屏障指令,因为你还提到了CPU重新排序问题.我碰巧在与编译器工作者相同的实验室工作(至少对于我们的平台之一),并且已经与内在函数的实现者交谈过,我碰巧知道至少对于xlC编译器__isync()和__lwsync()(其余的原子内在函数)也是代码重新排序的障碍.在我们的自旋锁实现中,编译器可以看到这一点,因为我们的关键部分的这一部分是内联的.

但是,假设您没有使用自定义构建锁实现(就像我们碰巧那样,这可能不常见),并且只调用了一个通用接口,例如pthread_mutex_lock().在那里,编译器不会通知原型.我从未见过它表明代码不起作用

pthread_mutex_lock( &m ) ;
pSharedMem->someNonVolatileVar++ ;
pthread_mutex_unlock( &m ) ;

pthread_mutex_lock( &m ) ;
pSharedMem->someNonVolatileVar++ ;
pthread_mutex_unlock( &m ) ;

除非将变量更改为volatile,否则将无法正常工作.该增量将在每个背靠背代码块中具有加载/增量/存储序列,并且如果第一增量的值在第二个寄存器中保留,则将不能正确地操作.

似乎pthread_mutex_lock()的未知副作用可以保护这种背靠背增量示例不正常行为.

我在谈论自己的结论,在线程环境中这样的代码序列的语义并没有真正严格地涵盖在C或C++语言规范中.

jal*_*alf 11

简而言之,只要C++虚拟机上的可观察行为没有改变,就允许编译器根据需要重新排序或转换程序.C++标准没有线程概念,因此这个虚构的VM只运行一个线程.在这样一个假想的机器上,我们不必担心其他线程看到的内容.只要更改不改变当前线程的结果,所有代码转换都是有效的,包括重新排序跨序列点的内存访问.

理解为什么volatile似乎不是受互斥保护的数据的要求数据实际上是这个问题的一部分

Volatile确保一件事,只有一件事:每次都会从内存中读取一个volatile变量的读取 - 编译器不会认为该值可以缓存在寄存器中.同样,写入将写入内存.编译器不会将它保留在寄存器中"暂时将其写入存储器".

但就是这样.当发生写入时,将执行写入,并且当发生读取时,将执行读取.但它并不保证何时会发生这种读/写操作.正如通常那样,编译器可以按照它认为合适的方式对操作进行重新排序(只要它不会改变当前线程中的可观察行为,即虚构的C++ CPU所知道的行为).所以挥发性并不能真正解决问题.另一方面,它提供了我们并不真正需要的保证.我们不需要立即写出对变量的每一次写入,我们只是想确保在跨越边界之前将它们写出来.如果它们在那之前被缓存就好了 - 同样,一旦我们越过临界区边界,后续的写入可以再次缓存我们关心的所有 - 直到我们下次越过边界.如此动荡提供,我们并不需要太强有力的保证,但不提供一个我们需要(即读/写操作将不会重新排序)

因此,要实现关键部分,我们需要依赖编译器魔术.我们必须告诉它"好吧,暂时忘记C++标准,如果你严格遵循它,我不关心它会允许哪些优化.你不能重新排序跨越这个边界的任何内存访问".

关键部分通常通过特殊的编译器内在函数(基本上是编译器可以理解的特殊函数)实现,1)强制编译器避免重新排序该内部函数,2)使其发出必要的指令以使CPU尊重相同的边界(因为CPU也重新排序指令,并且没有发出内存屏障指令,我们冒险CPU执行相同的重新排序,我们只是阻止编译器这样做)

  • +1好奇好奇**as*if*规则的***可观察行为***是否仍适用于C++ 11中的单个线程,还是扩展到VM中的所有线程? (2认同)