C++11 引入了标准化的内存模型。这是什么意思?它将如何影响 C++ 编程?

标签 c++ multithreading c++11 language-lawyer memory-model

C++11 引入了标准化的内存模型,但这究竟是什么意思?它将如何影响 C++ 编程?

This article (by Gavin Clarke 引用 Herb Sutter ) 说,

The memory model means that C++ code now has a standardized library to call regardless of who made the compiler and on what platform it's running. There's a standard way to control how different threads talk to the processor's memory.

"When you are talking about splitting [code] across different cores that's in the standard, we are talking about the memory model. We are going to optimize it without breaking the following assumptions people are going to make in the code," Sutter said.



好吧,我可以记住这个和在线可用的类似段落(因为我从出生起就有自己的内存模型:P),甚至可以发布作为对其他人提出的问题的回答,但说​​实话,我不完全理解这。

C++ 程序员以前也开发过多线程应用程序,那么到底是 POSIX 线程,还是 Windows 线程,还是 C++11 线程呢?有什么好处?我想了解底层细节。

我也有这种感觉,即 C++11 内存模型与 C++11 多线程支持有某种关系,因为我经常看到这两者在一起。如果是,具体如何?为什么他们应该有关系?

由于我不知道多线程内部是如何工作的,以及内存模型的一般含义,请帮助我理解这些概念。 :-)

最佳答案

首先,您必须学会像语言律师一样思考。
C++ 规范没有提及任何特定的编译器、操作系统或 CPU。它引用了抽象机器,它是实际系统的概括。在语言律师的世界里,程序员的工作是为抽象机器编写代码;编译器的工作是在具体机器上实现该代码。通过严格按照规范编码,您可以确定您的代码无需修改即可在任何具有兼容 C++ 编译器的系统上编译和运行,无论是现在还是 50 年后。
C++98/C++03 规范中的抽​​象机基本上是单线程的。因此,不可能编写相对于规范“完全可移植”的多线程 C++ 代码。该规范甚至没有说明内存加载和存储的原子性或加载和存储可能发生的顺序,更不用说互斥锁之类的事情了。
当然,您可以在实践中为特定的具体系统编写多线程代码——比如 pthreads 或 Windows。但是对于 C++98/C++03 没有标准的方法来编写多线程代码。
C++11 中的抽象机在设计上是多线程的。它还具有明确定义的内存模型;也就是说,它说明了编译器在访问内存时可以做什么和不可以做什么。
考虑以下示例,其中两个线程同时访问一对全局变量:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;
线程 2 可能输出什么?
在 C++98/C++03 下,这甚至不是 Undefined Behavior;这个问题本身毫无意义,因为标准没有考虑任何称为“线程”的东西。
在 C++11 下,结果是 Undefined Behavior,因为加载和存储通常不需要是原子的。这似乎没有太大的改进......而就其本身而言,事实并非如此。
但是使用 C++11,你可以这样写:
           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;
现在事情变得更有趣了。首先,定义了这里的行为。线程 2 现在可以打印 0 0 (如果它在线程 1 之前运行),37 17 (如果它在线程 1 之后运行),或 0 17 (如果它在线程 1 分配给 x 之后但在分配给 y 之前运行)。
不能打印的是37 0 ,因为 C++11 中原子加载/存储的默认模式是强制执行顺序一致性。这只是意味着所有加载和存储都必须“好像”它们按照您在每个线程中编写它们的顺序发生,而线程之间的操作可以根据系统的需要进行交错。所以原子的默认行为为加载和存储提供原子性和排序。
现在,在现代 CPU 上,确保顺序一致性的成本可能很高。特别是,编译器很可能会在此处的每次访问之间发出全面的内存屏障。但是如果你的算法可以容忍无序加载和存储;即,如果它需要原子性但不需要排序;即,如果它可以容忍 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 越现代,它就越有可能比前面的示例更快。
最后,如果您只需要按顺序保持特定的加载和存储,您可以编写:
           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;
这将我们带回到有序加载和存储 - 所以 37 0不再是可能的输出——但它以最小的开销实现。 (在这个简单的例子中,结果与完全成熟的顺序一致性相同;在更大的程序中,它不会。)
当然,如果你想看到的唯一输出是 0 037 17 ,您可以在原始代码周围包裹一个互斥锁。但是,如果您已经读到这里,我敢打赌您已经知道它是如何工作的,并且这个答案已经比我预期的要长:-)。
所以,底线。互斥体很棒,C++11 对它们进行了标准化。但有时出于性能原因,您需要较低级别的原语(例如,经典的 double-checked locking pattern )。新标准提供了诸如互斥体和条件变量之类的高级小工具,并且还提供了诸如原子类型和各种类型的内存屏障之类的低级小工具。因此,现在您可以完全使用标准指定的语言编写复杂的高性能并发例程,并且您可以确定您的代码将在今天和明天的系统上编译和运行不变。
坦率地说,除非您是专家并且正在处理一些严肃的低级代码,否则您可能应该坚持使用互斥锁和条件变量。这就是我打算做的。
有关此内容的更多信息,请参阅 this blog post .

关于C++11 引入了标准化的内存模型。这是什么意思?它将如何影响 C++ 编程?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6319146/

相关文章:

c++ - 使用用户定义的文字初始化 constexpr 数组

c++ - 我应该如何在工厂类中存储弱指针列表?

C++ 构造函数继承无法正常工作

c++ - Pthread_create() 不正确的启动例程参数传递

c++ - 是否可以在运行时检查 msvcrt.dll 中的 _controlfp_s?

python - Python 中的多线程

c - C中的多线程服务器/客户端实现

c++ - std::unique_ptr 的推荐用法

c++ - “lambda capture”在常规功能上吗?

c++ - 如何为 Kronecker-Produkt 实现张量类