技术思考25: 什么是C++内存模型?

79 阅读3分钟

1 内存模型说明了编译器在访问内存时可以做什么不能做什么

在c++98/03的世界中, 认为机器是单线程的,

c++规范中没有内存原子性操作的说明,

在那个时代, 编写多线程程序依赖具体的系统(如pthread, windows),

多线程程序是无法满足移植性的要求的.

 

在c++11中, 标准开始定义内存模型,

内存模型说明了编译器在访问内存时, 可以做什么, 不能做什么.

 

2 c++ 98标准下的多线程程序

如下是一段c++ 98标准下的多线程程序,

多线程依赖pthread


1.   
#include <pthread.h>

2. #include <iostream>

3. 

4.  *// 全局变量*

5. int x, y;

6. 

7.  *// 线程函数原型*

8. voidthread1(void*);

9. voidthread2(void*);

10. 

11.  *// 线程1设置x和y的值*

12. voidthread1(void*) {

13.     x = 17;

14.     y = 37;

15.     return NULL;

16. }

17. 

18.  *// 线程2打印x和y的值*

19. voidthread2(void*) {

20.     std::cout << "x: " << x << ", y: " << y << std::endl;

21.     return NULL;

22. }

23. 

24. int main() {

25.      *// 创建线程1*

26.     pthread_t tid1;

27.     pthread_create(&tid1, NULL, thread1, NULL);

28. 

29.      *// 创建线程2*

30.     pthread_t tid2;

31.     pthread_create(&tid2, NULL, thread2, NULL);

32. 

33.      *// 等待线程1完成*

34.     pthread_join(tid1, NULL);

35.     

36.      *// 等待线程2完成*

37.     pthread_join(tid2, NULL);

38. 

39.     return 0;

40. }

 

3 c++11标准下的实现

下面是c++11标准下, 使用原子store/load来访问内存

 

1.   
#include <iostream>

2. #include <thread>

3. #include <atomic>

4. 

5.  *// 全局原子变量*

6. std::atomic<int> x, y;

7. 

8.  *// 线程函数*

9. void thread1() {

10.     x.store(17);   *// 设置x*

11.     y.store(37);   *// 设置y*

12. }

13. 

14. void thread2() {

15.     int x_value = x.load();   *// 获取x*

16.     int y_value = y.load();   *// 获取y*

17.     std::cout << "x: " << x_value << ", y: " << y_value << std::endl;

18. }

19. 

20. int main() {

21.      *// 创建线程1*

22.     std::thread t1(thread1);

23. 

24.      *// 创建线程2*

25.     std::thread t2(thread2);

26. 

27.      *// 等待线程1完成*

28.     t1.join();

29. 

30.      *// 等待线程2完成*

31.     t2.join();

32. 

33.     return 0;

34. }

 ```

 

上面的程序可能打印0 0, 37 17, 0 17,

但是它不能打印的是 37 0 ,因为 C++ 11 中原子加载/存储的默认模式是强制顺序一致性。这只是意味着所有加载和存储都必须按照在每个线程中编写它们的顺序发生,

而线程之间的操作按照系统安排的方式交错。

因此,atomic的默认行为既提供了原子性,也提供了加载和存储的排序。

 

### 4 提升程序性能 放弃线程内有序性

在现代cpu上, 确保每个线程的顺序一致性代价高昂,

编译器可能需要在每次访存操作时, 发出内存屏障,

如果你只需要原子性, 而不需要有序性,

也就是说, 你可以容忍线程内乱序执行的话,

可以采用如下方式访问内存
```c++

1.   
    x.store(17, std::memory_order_relaxed);   *// 设置x*

2.     y.store(37, std::memory_order_relaxed);   *// 设置y*

3. 

4.     int x_value = x.load(std::memory_order_relaxed);   *// 获取x*

5. int y_value = y.load(std::memory_order_relaxed);   *// 获取y*

 ```

其他更多关键字包括:

std::memory_order_acquire 读取内存排序

std::memory_order_release 写入内存排序

 

### 5 c++中的atomic等效于java的volatile关键字

与java的内存模型比较,

c++中的atomic等效于java的volatile关键字