关注公众号获取更多信息:
释放消费顺序
若线程 A 中的原子存储带标签 memory_order_release 而线程 B 中来自同一对象的读取存储值的原子加载带标签 memory_order_consume ,则线程 A 视角中先发生于原子存储的所有内存写入(非原子和宽松原子的),会在线程 B 中该加载操作所携带依赖进入的操作中变成可见副效应,即一旦完成原子加载,则保证线程B中,使用从该加载获得的值的运算符和函数,能见到线程 A 写入内存的内容。
同步仅在释放和消费同一原子对象的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。
所有异于 DEC Alpha 的主流 CPU 上,依赖顺序是自动的,无需为此同步模式产生附加的 CPU 指令,只有某些编译器优化收益受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。
此顺序的典型使用情况,涉及对很少被写入的数据结构(安排表、配置、安全策略、防火墙规则等)的共时读取,和有指针中介发布的发布者-订阅者情形,即当生产者发布消费者能通过其访问信息的指针之时:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的例子之一是 rcu 解引用。
细粒度依赖链控制可参阅 std::kill_dependency 及 [[carries_dependency]] 。
注意到 2015 年 2 月为止没有产品编译器跟踪依赖链:均将消费操作提升为获得操作。
释放消费顺序的规范正在修订中,而且暂时不鼓励使用 memory_order_consume 。 | (C++17 起) |
此示例演示用于指针中介的发布的依赖定序同步:int data不由数据依赖关系关联到指向字符串的指针,从而其值在消费者中未定义。
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_consume)))
;
assert(*p2 == "Hello"); // 绝无出错:*p2 从 ptr 携带依赖
assert(data == 42); // 可能也可能不会出错:data 不从 ptr 携带依赖
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
序列一致顺序
带标签 memory_order_seq_cst 的原子操作不仅以与释放/获得顺序相同的方式排序内存(在一个线程中先发生于存储的任何结果都变成进行加载的线程中的可见副效应),还对所有带此标签的内存操作建立单独全序。
正式而言, 每个加载原子对象 M 的 memory_order_seq_cst 操作 B ,观测到以下之一:
若存在 memory_order_seq_cst的 std::atomic_thread_fence 操作 X 先序于 B ,则 B 观测到以下之一:
设有一对 M 上的原子操作,称之为 A 和 B ,这里 A 写入、 B 读取 M 的值,若存在二个 memory_order_seq_cst 的 std::atomic_thread_fence X 和 Y ,且若 A 先序于 X , Y 先序于 B ,且 X 在单独全序中先出现于 Y ,则 B 观测到二者之一:
设有一对 M 上的原子操作,称之为 A 和 B ,若符合下列条件之一,则 M 的修改顺序中 B 先发生于 A
注意这表明: 1) 只要不带 memory_order_seq_cst 标签的原子操作进入局面,则立即丧失序列一致性 2) 序列一致栅栏仅为栅栏自身建立全序,而不为通常情况下的原子操作建立(先序于不是跨线程关系,不同于先发生于) | (C++20 前) |
正式而言, 某原子对象 M 上的原子操作连贯先序于 M 上的另一原子操作 B ,若下列任一为真: 1) A 是修改,而 B 读取 A 所存储的值 2) A 在 M 的修改顺序中前于 B 3) A 读取原子操作 X 所存储的值,而 X 在修改顺序中前于 B ,且 A 与 B 不是同一读修改写操作 4) A 连贯先序于 X ,而 X 连贯先序于 B 所有 memory_order_seq_cst 操作,包括栅栏上,有单独全序 S ,它满足下列制约: 1) 若 A 与 B 为 memory_order_seq_cst 操作,而 A 强先发生于 B ,则 A 在 S 中前于 B 2) 对于对象 M 上的每对原子操作 A 与 B ,其中 A 连贯先序于 B : a) 若 A 与 B 都是 memory_order_seq_cst 操作,则 S 中 A 前于 B b) 若 A 是 memory_order_seq_cst 操作,而 B 先发生于 memory_order_seq_cst 栅栏 Y ,则 S 中 A 前于 Y c) 若 memory_order_seq_cst 栅栏 X 先发生于 A ,而 B 为 memory_order_seq_cst 操作,则 S 中 X 前于 B d) 若 memory_order_seq_cst 栅栏 X 先发生于 A ,而 B 先发生于 memory_order_seq_cst 栅栏 Y ,则 S 中 X 前于 Y 正式定义确保: 1) 单独全序与任何原子对象的修改顺序一致 2) memory_order_seq_cst 加载从最后的 memory_order_seq_cst 修改,或从某个不先发生于顺序中之前的 memory_order_seq_cst 修改操作的非 memory_order_seq_cst 修改之一获取其值。 单独全序可能与先发生于不一致。这允许 memory_order_acquire 与 memory_order_release 在某些 CPU 上的更高效实现。当 memory_order_acquire 及 memory_order_release 与 memory_order_seq_cst 混合时,这能产生惊异的结果。 例如,对于初值为零的 x 与 y , // 线程 1 : x.store(1, std::memory_order_seq_cst); // A y.store(1, std::memory_order_release); // B // 线程 2 : r1 = y.fetch_add(1, std::memory_order_seq_cst); // C r2 = y.load(std::memory_order_relaxed); // D // 线程 3 : y.store(3, std::memory_order_seq_cst); // E r3 = x.load(std::memory_order_seq_cst); // F 允许这些操作产生 r1 == 1 && r2 == 3 && r3 == 0 的结果,其中 A 先发生于 C ,但 memory_order_seq_cst 的单独全序 C-E-F-A 中 C 前于 A (见 Lahav 等)。 注意: 1) 一旦不带 memory_order_seq_cst 标签的原子操作进入局面,程序的序列一致保证就会立即丧失 2) 多数情况下, memory_order_seq_cst 原子操作相对于同一线程所进行的其他原子操作可重排 | (C++20 起) |
若在多生产者-多消费者的情形中,且所有消费者都必须以相同顺序观察到所有生产者的动作出现,则可能必须有序列顺序。
全序列顺序在所有多核系统上要求完全的内存栅栏 CPU 指令。这可能成为性能瓶颈,因为它强制受影响的内存访问传播到每个核心。
此示例演示序列一直顺序为必要的场合。任何其他顺序都可能触发assert,因为可能令线程c和d观测到原子对象x和y以相反顺序更改。
#include <thread>
#include <atomic>
#include <cassert>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x()
{
x.store(true, std::memory_order_seq_cst);
}
void write_y()
{
y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y()
{
while (!x.load(std::memory_order_seq_cst))
;
if (y.load(std::memory_order_seq_cst)) {
++z;
}
}
void read_y_then_x()
{
while (!y.load(std::memory_order_seq_cst))
;
if (x.load(std::memory_order_seq_cst)) {
++z;
}
}
int main()
{
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join(); b.join(); c.join(); d.join();
assert(z.load() != 0); // 决不发生
}
与 volatile 的关系
在执行线程中,不能将通过 volatile 泛左值的访问(读和写)重排到同线程内先序于或后序于它的可观测副效应(包含其他 volatile 访问)后,但不保证另一线程观察到此顺序,因为 volatile 访问不建立线程间同步。
另外, volatile 访问不是原子的(共时的读和写是数据竞争),且不排序内存(非 volatile 内存访问可以自由地重排到 volatile 访问前后)。
一个值得注意的例外是 Visual Studio ,其中默认设置下,每个 volatile 写拥有释放语义,而每个 volatile 读拥有获得语义,故而可将 volatile 对象用于线程间同步。标准的 volatile 语义不可应用于多线程编程,尽管它们在应用到 sig_atomic_t 对象时,足以与例如运行于同一线程的 std::signal 处理函数交流。