c++ - 单个负载是否与多个商店同步?
问题描述
以下是来自C++ Standard - Memory Order的引用:
如果线程 A 中的原子存储标记为 memory_order_release,并且线程 B 中来自同一变量的原子加载标记为 memory_order_acquire,则从线程的角度来看,在原子存储之前发生的所有内存写入(非原子和宽松原子) A,在线程 B 中成为可见的副作用。也就是说,一旦原子加载完成,线程 B 保证可以看到线程 A 写入内存的所有内容。
仅在释放和获取相同原子变量的线程之间建立同步。与同步线程中的一个或两个线程相比,其他线程可以看到不同的内存访问顺序。
考虑一个原子变量v
和以下步骤:
- 使用中的线程
A
存储v
memory_order_release
- 使用中的线程
B
存储v
memory_order_release
- 线程从使用中
C
加载v
memory_order_acquire
以下陈述是否正确:“线程C
保证看到所有线程A
或B
写入内存。”
编辑:我在这里移动我的评论以使这一点更清楚。
我在那里的 C++ 引用没有说B
必须阅读A
. 它所说的只是在同一个变量上释放/获取A
。B
这就是我在这三个步骤中所做的:释放一些东西,并A
获得一些东西。它在规范中的哪里说,获取与最后一个版本匹配,而不一定是在那之前的任何内容?B
C
解决方案
来自的加载与写入返回v
值的两个存储中的任何一个同步。v.load()
标准本身使这一点更加明确。请参阅n3337 atomics.order p2:“对原子对象 M 执行释放操作的原子操作 A 与对 M 执行获取操作的原子操作 B 同步,并从以 A 为首的释放序列中的任何副作用获取其值。”
为了说明这一点,这里有一个例子:
int a,b;
std::atomic<int> v = 0;
void thread_A() {
a = 42;
v.store(10, std::memory_order_release);
}
void thread_B() {
b = 17;
v.store(20, std::memory_order_release);
}
void thread_C() {
switch (v.load(std::memory_order_acquire)) {
case 10:
// thread A must have done this store
std::cout << a; // ok, prints 42
std::cout << b; // UB, data race
break;
case 20:
// thread B must have done this store
std::cout << a; // UB, data race
std::cout << b; // ok, prints 17
break;
case 0:
// neither A or B has done its store
std::cout << a; // UB, data race
std::cout << b; // UB, data race
break;
}
}
所以如果v.load()
线程 C 返回 10,我们从程序的逻辑中知道这个值一定是由v.store()
线程 A 存储的;我们的程序中没有其他地方可以做到这一点。由于该存储上的发布顺序,线程 A 之前所做的所有写入也是可见的。我们可以安全地从非原子变量中读取a
,并且可以保证获得值42
。
更正式地说, 与返回 10 的同步,并且在v.store(10)
之前排序,因此线程间发生在之前(intro.multithread p11)。And is sequenced before,正如我们所说的线程间发生在之前,所以线程间发生在之前;特别是发生在(p12) 之前,因此没有数据竞争(p21)。此外,现在对于(p13) 来说是一个可见的副作用,并且没有其他副作用可以看到,因此in的评估值v.load()
v.load()
cout << a
v.store(10)
cout << a
a = 42
v.store(10)
cout << a
a = 42
cout << a
a = 42
cout << a
a = 42
cout << a
a
a
cout << a
应为 存储的值a = 42
,即42
。
但是在这种情况下,由于v.load()
返回的是 10 而不是 20,所以我们不知道v.store()
in 线程 B 是否发生了。也许它确实并且已经被线程 A 中的商店覆盖。或者它根本没有发生。所以我们不能证明b = 17
之前发生过 cout << b
,反之亦然,因此这是一个导致未定义行为的数据竞争。
返回 20的情况v.load()
类似,但相反。如果v.load()
返回 0,则两个存储都没有发生,访问a
或是数据竞争b
。
如您所见,这仅在线程 A 和 B 存储不同值时才有用。如果我们改变程序让 A 和 B 都做v.store(10, std::memory_order_release)
,那么让线程 C 观察它不会v.load() == 10
告诉我们两个线程中的哪一个做了存储。负载与其中一个同步,但我们不知道是哪个。因此,在这种情况下,线程 C 不能安全地访问a
或b
,因为任何一个都可能是处于数据竞争中的那个。
脱离上下文的 cppreference 文本可能听起来像是仅仅做的行为v.load(std::memory_order_acquire)
就会导致线程实际上等待其他线程中的一些或所有其他存储完成,有点像互斥锁或 std::latch
. 你不会是第一个以这种方式误读它的人。但这没有任何意义——负载毕竟只是负载。它返回v
恰好在该特定时刻具有的值,而不会阻塞或等待来自任何其他线程的任何事件。
推荐阅读
- python - 消除 json/dictionary 中的所有空结构
- angular - Angular 2+ 自定义表单验证打破了表单方法
- javascript - 在不同屏幕尺寸上放大的 HTML Jumbotron 图像
- c++ - 非恒定操作的 DFS 时间复杂度
- reactjs - 如何导出在反应中动态加载的对象数组
- python - 如何聚合具有许多字符串列的 Pandas DF?
- python - 使用 Python 自定义数据库查询
- php - 使用 .env 拒绝用户 ''@'localhost' 的访问
- python - 随着时间的推移绘制多维数据(超过 3 个)
- c++ - String Class C++ 为什么反转的字符串没有正确构建?