内存序
LDK Lv5

什么是内存序

内存顺序是指在并发编程中, 对内存读写操作的执行顺序。这个顺序可以被编译器和处理器进行优化, 可能会与代码中的顺序不同, 这被称为指令重排。比如下面代码:

1
2
3
4
std::atomic<int> x{0}, y{0};

x.store(10);
y.store(20);

注意:虽然上面的变量x, y都是std::atomic类型,但是实际上程序在执行时可能是下面的情况:

1
2
y.store(20);
x.store(10);

也就是说,经过指令重排,实际上的执行顺序已经发生改变。

除此之外,现代CPU的多级缓存体系也会影响指令的执行。虽然MESI协议保证了缓存一致性,但它只保证最终一致性,不保证实时性

C++的6种内存序

所谓内存序,实际上是指:对单个线程内多个原子操作效果可见性的一种顺序保证。

memory_order_seq_cst: 顺序一致性

最强保证。所有操作按照一个全局顺序执行,相当于在每个原子操作后面都加了内存屏障。相对的,性能也会下降

1
2
3
4
5
6
7
8
9
10
11
std::atomic<int> x{0}, y{0};

// 线程1
x.store(1, std::memory_order_seq_cst); // 屏障
y.store(1, std::memory_order_seq_cst); // 屏障

// 线程2
if (y.load(std::memory_order_seq_cst) == 1) { // 屏障
// 这里x一定为1. assert一定为true
assert(x.load(std::memory_order_seq_cst) == 1);
}

memory_order_releasememory_order_acquire: 释放-获取语义

这两个需要配对使用。建立synchronizes-with关系(跨线程的同步关系)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::atomic<bool> ready{false};
int payload = 0;

// 发布线程 - 写操作使用release
void publisher() {
payload = 42; // 非原子写入
ready.store(true, std::memory_order_release); // 发布:保证之前的写入对获取线程可见
}

// 订阅线程 - 读操作使用acquire
void subscriber() {
while (!ready.load(std::memory_order_acquire)) { // 获取:看到release存储后,保证看到之前的所有写入
std::cout << "ready is false, subscriber is waiting" << std::endl;
}
// 这里payload一定是42
std::cout << payload << std::endl;
}

注意:有时候又会造成误伤

请看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
std::atomic<int> net_con{0};
std::atomic<int> has_alloc{0};
char buffer[1024];
char file_content[1024];

void release_thread(void) {
// 对buffer的操作与两个原子变量无关。
sprintf(buffer, "%s", "something_to_read_tobuffer");

// net_con表示接收到的链接
net_con.store(1, std::memory_order_release);
// 标记alloc memory for connection
has_alloc.store(1, std::memory_order_release);
}

void acquire_thread(void) {
// 这个是与两个原子变量完全无关的操作。
if (strstr(file_content, "auth_key =")) {
// fetch user and password
}

while (!has_alloc.load(std::memory_order_acquire));
bool v = has_alloc.load(std::memory_order_acquire);
if (v) {
net_con.load(std::memory_order_relaxed);
}
}

从上面代码中可以看出,bufferfile_content的使用,与两个原子变量就目前的这段简短的代码而言是没有任何联系的。按理说,这两部分的代码是可以放到任何位置执行的。但是,由于使用了release-acquire,那么会导致的情况就是,buffer和file_content的访问都被波及。

memory_order_acq_rel: 获取-释放语义

用于读-修改-写操作,同时具有acquire和release语义。

1
2
3
4
5
6
7
8
9
std::atomic<int> counter{0};

void increment() {
counter.fetch_add(1, std::memory_order_acq_rel);
// 相当于:
// 1. 获取(acquire)其他线程的修改
// 2. 执行加法
// 3. 释放(release)结果给其他线程
}

memory_order_relaxed: 松散排序

只保证原子性,不保证顺序。性能最好,但最难正确使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <atomic>
#include <iostream>
#include <vector>
#include <thread>

std::atomic<int> counter{0};

// 适合计数器场景
void increment_counter() {
counter.fetch_add(1, std::memory_order_relaxed);
}

// 危险的使用方式!
std::atomic<bool> flag{false};
int data = 0;

void dangerous_writer() {
data = 42;
flag.store(true, std::memory_order_relaxed); // 可能重排到data = 42之前!
}

void dangerous_reader() {
while (!flag.load(std::memory_order_relaxed)) {
// data可能不是42!
printf("data: %d\n", data);
}
std::cout << "data: " << data << std::endl;
}

int main() {
std::thread t2(dangerous_reader);
std::thread t1(dangerous_writer);

t1.join();
t2.join();

return 0;
}
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 96.6k 访客数 访问量