c++ - 序列点是否会阻止代码跨临界区边界重新排序?

标签 c++ multithreading concurrency 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.

如果假设生成的代码是按程序顺序创建的,那么仍然需要适当的硬件内存屏障,例如 isync、lwsync、.acq、.rel。对于这个问题,我假设互斥锁实现会处理这部分,提供保证 pSharedMem 读取和写入都发生在 get 之后和 release() 之前 [但是周围的读取和写入可以正如我所期望的那样进入关键部分是互斥体实现的规范]。我还将假设在适当的情况下在互斥体实现中使用了 volatile 访问,但 volatile 不用于受互斥体保护的数据(了解为什么 volatile 似乎不是受互斥体保护的数据的要求实际上是这个问题)。

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

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

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

编辑:(回应 Jalf 的回复)

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

但是,假设您没有使用自定义构建锁实现(就像我们碰巧那样,这很可能不常见),而只是调用了一个通用接口(interface),例如 pthread_mutex_lock()。除了原型(prototype)之外,编译器没有得到任何通知。我从未见过它暗示代码将不起作用

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++ 语言规范中。

最佳答案

简而言之,只要 C++ 虚拟机上的可观察行为不改变,编译器就可以随意对程序进行重新排序或转换。 C++ 标准没有线程的概念,所以这个虚构的 VM 只运行一个线程。而在这样一个虚构的机器上,我们不必担心 other 线程会看到什么。只要更改不改变 当前 线程的结果,所有代码转换都是有效的,包括跨序列点重新排序内存访问。

understanding why volatile does not appear to be a requirement for the mutex protected Data is really part of this question

Volatile 确保了一件事,而且只有一件事:从 volatile 变量中读取的内容每次都会从内存中读取——编译器不会假定该值可以缓存在寄存器中。同样,写入将被写入内存。编译器“在将其写入内存之前”不会将其保存在寄存器中。

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

所以要实现临界区,我们需要依靠编译器的魔法。我们必须告诉它“好吧,暂时忘记 C++ 标准,我不在乎如果你严格遵守它会允许什么优化。你不能重新排序任何内存跨越此边界的访问”。

关键部分通常通过特殊的编译器内在函数(本质上是编译器理解的特殊函数)实现,这 1)强制编译器避免重新排序该内在函数,2)使其发出必要的指令以获取 CPU尊重相同的边界(因为 CPU 也会重新排序指令,并且在不发出内存屏障指令的情况下,我们会冒着 CPU 执行与我们刚刚阻止编译器执行相同的重新排序的风险)

关于c++ - 序列点是否会阻止代码跨临界区边界重新排序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1614651/

相关文章:

c++ - cmake:如何告诉库在哪里可以找到依赖项

c++ - SDL_Surface* 作为 C++ 中的构造函数参数

multithreading - 是否可以 “cross collapse”并行循环?

c++ - 段错误——怎么会!

c# - 生产者-消费者示例,都在 lock(this) block 中

java - 不允许在主线程之外调用 SetRefreshing(false)。解决方法?

java - 在没有 IllegalMonitorStateException 的情况下解锁 ReentrantLock

concurrency - goroutine 是如何工作的?

multithreading - (术语) "Contended"与 "Contented"锁

c++ - 包含 vector 时 Visual Studio 崩溃