Er_HU

但求风浪,莫问前程。

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

引言

C++ 11 引入了标准的内存模型,但这个标准的内存模型意味着什么,它是如何影响 C++ 编程的?这个标准的内存模型总是和 C++ 多线程同时出现,一起讨论,但在 C++ 11 之前,多线程就一直在被我们使用,它们之间有什么必然关系吗?带着这些疑问,我们进入本文的主题。

标准的内存模型意味着什么

首先,C++ 标准不依赖于任何特定的编译器,操作系统,或 CPU。它依赖于抽象机(abstract machine),一个对现实计算机的抽象模型。在语言规范框架中,程序员的工作就是为抽象机编写代码,而编译器的工作就是将这些抽象机代码具象化为现实计算机能够运行的代码。只要你保证你的抽象机代码是严格按照 C++ 标准编写的,那么你就可以确信,它们可以使用部署在任何操作系统上的任何符合标准 C++ 编译器进行编译,而不需要作任何修改,哪怕是 100 年之后。

接着,C++98/C++03 标准中定义的抽象机是单线程的。因此在这个标准下,多线程的 C++ 代码 并不能保证是完全可移植的,这取决于具体的编译器实现。这个标准也没有提及任何关于内存读写的原子性操作或是顺序,以及类似互斥的概念。

当然,你完全可以为某个现实计算机(pthreads 或是 Windows)编写多线程代码,但这不是 C++98/C++03 所标准化的。

然后,C++ 11 标准中定义的抽象机是多线程的,其中还包括了一个非常好用的内存模型。具体来说就是,C++ 11 标准定义了编译器如何保证程序在访问内存的时候可以或者不可以做什么。

C++ 11 标准下的多线程

考虑下面这个例子,两个线程并发的访问两个全局变量:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

Thread 2 的输出是什么?

在 C++98/C++03 中,改行为是未定义的。问题本身也是毫无意义的,因为标准中并未定义任何线程相关的内容。
在 C++11 中,该行为也是未定义的,因为通常情况下,变量/内存的读写不一定是原子性的。相比于 C++98/C++03 似乎,好像...没什么多大不同?

但是在 C++ 11 中,你可以这么写:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

首先,该行为是经过定义的。Thread 2 可能打印 0 0(如果在 Thread 1 之前完成),可能打印 37 17(如果在 Thread 1 之后开始执行),或者 0 17(如果在 Thread 1 给 x 赋值之后执行,在 y 赋值之前结束)。

Thread 2 不会打印 37 0,因为 C++ 11 中 atomic 类型的 load 或者 store 默认是强制顺序一致性的。也就是意味着所有的 load/store 必须像你写代码的顺序一样执行操作,尽管线程之间的操作是交替进行的。所以 atomic 类型默认保证了原子性,以及顺序一致性。

在现代 CPU 上,保证顺序一致性的代价可能是昂贵的。如果你的算法能够允许你无序的 load/stroe,也就是只要保证原子性但不保证顺序一致性(比如在上述例子中输出 37 0 是允许的),那么你就可以这么写:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

相比于前一个例子,你的 CPU 越先进,上述例子就可能相对跑的越快。

回到前一个例子(有序的原子操作),如果你想要 Thread 2 只输出 0 0,或者 37 17,你可以用 mutex 管理你的代码,此处不展开了。

总结

Mutexes 是非常有用的,它在 C++ 11 标准中作了定义。但有时候因为性能考量,你需要更低层级的工具。
新的标准提供了高层及的工具,比如互斥,条件变量,同时也提供了原子类和多种内存模式等底层工具。你可以在 C++ 11 的标准框架下编写精心设计的,高性能的,复杂的并发系统,并且在 100 年后完美移植。

不过,除非你确实足够专业并且需要这些低层级的工具,你应当首先考虑互斥和条件变量。

引用

https://stackoverflow.com/questions/31978324/what-exactly-is-stdatomic?r=SearchResults

posted on 2024-06-27 13:09  Er_HU  阅读(5)  评论(0编辑  收藏  举报