Zih*_*Liu 2 c++ mutex atomic memory-model stdatomic
根据C++ Reference,mutex.lock()是一个memory_order_acquire操作,并且mutex.unlock()是一个memory_order_release操作。
然而,memory_order_acquire和memory_order_release只对非原子和宽松原子操作有效。
memory_order:释放-获取cppreference 上的排序
如果线程 A 中的原子存储被标记
memory_order_release,并且线程 B 中来自同一变量的原子加载被标记memory_order_acquire,则从线程 A 的角度来看,所有内存写入(非原子和宽松原子)发生在原子存储之前,在线程 B 中变得可见的副作用
C++中的互斥体能否保证原子操作的可见性?示例如下。代码可以A在 , 之前重新排序mu.lock(),并且线程b读x为 吗false?
#include <thread>
#include <atomic>
#include <cassert>
#include <iostream>
#include <unistd.h>
std::atomic<bool> x = {false};
std::mutex mu;
void write_x(){
mu.lock();
std::cout << "write_x" << std::endl;
x.store(true, std::memory_order_release);
mu.unlock();
}
void read_x() {
mu.lock();
std::cout << "read_x" << std::endl;
assert(x.load(std::memory_order_acquire)); // A
mu.unlock();
}
int main() {
std::thread a(write_x);
usleep(1);
std::thread b(read_x);
a.join(); b.join();
return 0;
}
Run Code Online (Sandbox Code Playgroud)
TL:DR:“所有内存写入”意味着所有,而不仅仅是提到的种类,但措辞令人困惑。可能只是想指出,即使是非原子和宽松的原子操作在同步中也是安全可见的,但措辞中缺少“包括”一词。
请注意,cppreference 是一个旨在解释该标准的 wiki。 它不是规范的技术语言,有时甚至用与 ISO C++ 标准不同的术语来解释事物。
它通常非常好,但不要在某些事情看起来很奇怪时就认为它是完美的。从周围的上下文(和理智)来看,就像段落中的最后一句没有限定条件的“一切”一样,这仍然是相当明显的意思。
ISO C++ 很清楚。“看到”释放操作的获取操作创建同步关系。 释放之前的所有内容对于获取操作之后的代码都是可见的。
因此,就访问全局一致共享内存状态的操作的模型而言,获取操作会阻止所有内容在它们之前重新排序。包括release和seq_cst操作。(请注意,cppreference 的这一部分没有提及任何重新排序,只是为了保证可见性。对全局一致状态的访问的本地重新排序实际上是真实 CPU 的工作方式,因此通常更方便地以这种方式描述事物,就像你在问题中所做的那样。)
这意味着 C++ 对获取和释放的定义与标准术语相匹配,没有疯狂的神奇异常。 https://preshing.com/20120913/acquire-and-release-semantics/
请注意,有些人使用“宽松原子”来描述所有弱于 的排序seq_cst。示例:赫伯·萨特 (Herb Sutter) 在讨论这个问题时就这样使用了它。
这可能就是 cppreference 定义中的含义,但我不知道为什么他们想要排除seq_cst. 所有原子和非原子操作都是有序的。所以也许他们的意思是mo_relaxed,只是想指出即使是那些也是有序/可见的。
(seq_cst可以说已经对其他一切进行了排序,因此“当然”它是根据获取和释放操作进行排序的。但这个原因似乎不太可能。)
如果是为了强调弱序也由它排序的事实,他们应该写“包括非原子和宽松原子”。如果没有“包括”一词,该措辞可以被理解为仅暗示非原子和宽松原子。只有了解大局以及什么是理智的、什么是不理智的,才能给你正确的解读。
需要准确理解的技术写作经常会使用“包括但不限于”这一短语。
另请注意,您的示例仍然可以触发断言,只是不是出于您担心的原因。
如果线程a启动缓慢,线程b可以先进入其临界区并x在另一个线程中的打印+存储发生之前进行打印+读取。
编写这样的玩具示例的通常方法是一个循环,该循环在获取负载上旋转,直到看到一个值,例如data_read在您关心的存储之后由释放操作存储的标志。这样您就知道读取端在与写入端的释放操作同步的获取操作之后运行。