猿问

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

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

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

这篇文章(引用Herb SutterGavin Clarke)说,

内存模型意味着C ++代码现在有一个标准化的库可以调用,无论是谁编译器以及它运行的平台。有一种标准方法可以控制不同线程与处理器内存的对话方式。

“当你谈论在标准中的不同内核之间分割[代码]时,我们正在谈论内存模型。我们将优化它,而不会破坏人们将在代码中做出的以下假设,” Sutter说。

好吧,我可以在网上记住这个和类似的段落(因为我从出生以来就拥有自己的记忆模型:P),甚至可以发布作为其他人提出的问题的答案,但说实话,我并不完全明白这个。

C ++程序员以前用于开发多线程应用程序,那么如果它是POSIX线程,Windows线程或C ++ 11线程,它又如何重要呢?有什么好处?我想了解低级细节。

我也觉得C ++ 11内存模型与C ++ 11多线程支持有某种关系,因为我经常将这两者结合在一起。如果是,究竟是怎么回事?他们为什么要相关?

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


慕慕森
浏览 605回答 3
3回答

慕勒3428872

首先,你必须学会像语言律师那样思考。C ++规范不引用任何特定的编译器,操作系统或CPU。它引用了一个抽象机器,它是实际系统的概括。在语言律师的世界里,程序员的工作就是为抽象机器编写代码;&nbsp;编译器的工作是在具体机器上实现该代码。通过严格按照规范进行编码,无论是今天还是50年后,您都可以确定您的代码无需在具有兼容C ++编译器的任何系统上进行编译和运行。C ++ 98 / C ++ 03规范中的抽象机器基本上是单线程的。所以不可能编写相对于规范“完全可移植”的多线程C ++代码。该规范甚至没有说明内存加载和存储的原子性或加载和存储可能发生的顺序,更不用说像互斥体这样的东西了。当然,您可以在实践中为特定的具体系统编写多线程代码 - 例如pthreads或Windows。但是没有标准的方法来为C ++ 98 / C ++ 03编写多线程代码。C ++ 11中的抽象机器是设计多线程的。它还有一个定义明确的内存模型&nbsp;;&nbsp;也就是说,它说明了在访问内存时编译器可能会做什么,也可能不会做什么。请考虑以下示例,其中两个线程同时访问一对全局变量:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Global &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;int&nbsp;x,&nbsp;y;Thread&nbsp;1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread&nbsp;2x&nbsp;=&nbsp;17;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;y&nbsp;<<&nbsp;"&nbsp;";y&nbsp;=&nbsp;37;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;x&nbsp;<<&nbsp;endl;线程2可能输出什么?在C ++ 98 / C ++ 03下,这甚至不是Undefined Behavior;&nbsp;问题本身毫无意义,因为标准没有考虑任何称为“线程”的东西。在C ++ 11下,结果是Undefined Behavior,因为加载和存储通常不需要是原子的。这可能看起来不是很大的改善......而且它本身并非如此。但是使用C ++ 11,你可以这样写:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Global &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;atomic<int>&nbsp;x,&nbsp;y;Thread&nbsp;1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread&nbsp;2x.store(17);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;y.load()&nbsp;<<&nbsp;"&nbsp;";y.store(37);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;x.load()&nbsp;<<&nbsp;endl;现在情况变得更有趣了。首先,定义了此处的行为。线程2现在可以打印0 0(如果它在线程1之前运行),37 17(如果它在线程1之后运行),或者0 17(如果它在线程1分配给x但在它分配给y之前运行)。它无法打印的是37 0,因为C ++ 11中原子加载/存储的默认模式是强制执行顺序一致性。这只意味着所有加载和存储必须“好像”它们按照您在每个线程中编写它们的顺序发生,而线程之间的操作可以交错,但系统喜欢。因此,atomics的默认行为为加载和存储提供了原子性和排序。现在,在现代CPU上,确保顺序一致性可能很昂贵。特别是,编译器可能会在每次访问之间发出完整的内存屏障。但是,如果您的算法可以容忍无序的加载和存储;&nbsp;即,如果它需要原子性而不是订购;&nbsp;即,如果它可以容忍37 0这个程序的输出,那么你可以这样写:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Global &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;atomic<int>&nbsp;x,&nbsp;y;Thread&nbsp;1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread&nbsp;2x.store(17,memory_order_relaxed);&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;y.load(memory_order_relaxed)&nbsp;<<&nbsp;"&nbsp;";y.store(37,memory_order_relaxed);&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;x.load(memory_order_relaxed)&nbsp;<<&nbsp;endl;CPU越现代,就越有可能比前一个例子更快。最后,如果您只需要按顺序保持特定的加载和存储,您可以编写:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Global &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;atomic<int>&nbsp;x,&nbsp;y;Thread&nbsp;1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Thread&nbsp;2x.store(17,memory_order_release);&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;y.load(memory_order_acquire)&nbsp;<<&nbsp;"&nbsp;";y.store(37,memory_order_release);&nbsp;&nbsp;&nbsp;cout&nbsp;<<&nbsp;x.load(memory_order_acquire)&nbsp;<<&nbsp;endl;这将我们带回有序的加载和存储 - 因此37 0不再是可能的输出 - 但它以最小的开销实现了这一点。(在这个简单的例子中,结果与完整的顺序一致性相同;在较大的程序中,它不会。)当然,如果您想要查看的唯一输出是0 0或者37 17,您可以在原始代码周围包装互斥锁。但是如果你已经读过这篇文章了,我打赌你已经知道它是如何工作的,这个答案已经比我预想的要长:-)。所以,底线。互斥体很棒,C ++ 11将它们标准化。但有时出于性能原因,您需要较低级别的基元(例如,经典的双重检查锁定模式)。新标准提供了高级小工具,如互斥锁和条件变量,它还提供低级小工具,如原子类型和各种内存屏障。因此,现在您可以完全使用标准指定的语言编写复杂的高性能并发例程,并且可以确定您的代码将在今天的系统和未来的系统上编译和运行。虽然坦率地说,除非您是专家并且正在处理一些严重的低级代码,否则您应该坚持使用互斥锁和条件变量。这就是我打算做的事情。有关这些内容的更多信息,请参阅此博客文章。

长风秋雁

这是一个多年前的问题,但是非常受欢迎,值得一提的是学习C ++ 11内存模型的绝佳资源。我认为总结他的演讲是没有意义的,以便再做一个完整的答案,但鉴于这是实际编写标准的人,我认为值得观看谈话。Herb Sutter有一个长达3个小时的关于C ++ 11内存模型的讨论,名为“atomic <> Weapons”,可在Channel9网站上找到 -&nbsp;第1&nbsp;部分和第2部分。这个讲座非常技术性,涵盖以下主题:优化,种族和记忆模型订购 - 什么:获取和发布订购 - 如何:互斥锁,原子和/或栅栏编译器和硬件的其他限制代码和性能:x86 / x64,IA64,POWER,ARM轻松的原子论谈话没有详细说明API,而是关于推理,背景,幕后和幕后(您是否知道轻松的语义被添加到标准中只是因为POWER和ARM不能有效地支持同步加载?)。
随时随地看视频慕课网APP
我要回答