3 回答
TA贡献1848条经验 获得超6个赞
首先,你必须学会像语言律师那样思考。
C ++规范不引用任何特定的编译器,操作系统或CPU。它引用了一个抽象机器,它是实际系统的概括。在语言律师的世界里,程序员的工作就是为抽象机器编写代码; 编译器的工作是在具体机器上实现该代码。通过严格按照规范进行编码,无论是今天还是50年后,您都可以确定您的代码无需在具有兼容C ++编译器的任何系统上进行编译和运行。
C ++ 98 / C ++ 03规范中的抽象机器基本上是单线程的。所以不可能编写相对于规范“完全可移植”的多线程C ++代码。该规范甚至没有说明内存加载和存储的原子性或加载和存储可能发生的顺序,更不用说像互斥体这样的东西了。
当然,您可以在实践中为特定的具体系统编写多线程代码 - 例如pthreads或Windows。但是没有标准的方法来为C ++ 98 / C ++ 03编写多线程代码。
C ++ 11中的抽象机器是设计多线程的。它还有一个定义明确的内存模型 ; 也就是说,它说明了在访问内存时编译器可能会做什么,也可能不会做什么。
请考虑以下示例,其中两个线程同时访问一对全局变量:
Global int x, y;Thread 1 Thread 2x = 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 2x.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中原子加载/存储的默认模式是强制执行顺序一致性。这只意味着所有加载和存储必须“好像”它们按照您在每个线程中编写它们的顺序发生,而线程之间的操作可以交错,但系统喜欢。因此,atomics的默认行为为加载和存储提供了原子性和排序。
现在,在现代CPU上,确保顺序一致性可能很昂贵。特别是,编译器可能会在每次访问之间发出完整的内存屏障。但是,如果您的算法可以容忍无序的加载和存储; 即,如果它需要原子性而不是订购; 即,如果它可以容忍37 0
这个程序的输出,那么你可以这样写:
Global atomic<int> x, y;Thread 1 Thread 2x.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 2x.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 0
或者37 17
,您可以在原始代码周围包装互斥锁。但是如果你已经读过这篇文章了,我打赌你已经知道它是如何工作的,这个答案已经比我预想的要长:-)。
所以,底线。互斥体很棒,C ++ 11将它们标准化。但有时出于性能原因,您需要较低级别的基元(例如,经典的双重检查锁定模式)。新标准提供了高级小工具,如互斥锁和条件变量,它还提供低级小工具,如原子类型和各种内存屏障。因此,现在您可以完全使用标准指定的语言编写复杂的高性能并发例程,并且可以确定您的代码将在今天的系统和未来的系统上编译和运行。
虽然坦率地说,除非您是专家并且正在处理一些严重的低级代码,否则您应该坚持使用互斥锁和条件变量。这就是我打算做的事情。
有关这些内容的更多信息,请参阅此博客文章。
TA贡献1757条经验 获得超7个赞
这是一个多年前的问题,但是非常受欢迎,值得一提的是学习C ++ 11内存模型的绝佳资源。我认为总结他的演讲是没有意义的,以便再做一个完整的答案,但鉴于这是实际编写标准的人,我认为值得观看谈话。
Herb Sutter有一个长达3个小时的关于C ++ 11内存模型的讨论,名为“atomic <> Weapons”,可在Channel9网站上找到 - 第1 部分和第2部分。这个讲座非常技术性,涵盖以下主题:
优化,种族和记忆模型
订购 - 什么:获取和发布
订购 - 如何:互斥锁,原子和/或栅栏
编译器和硬件的其他限制
代码和性能:x86 / x64,IA64,POWER,ARM
轻松的原子论
谈话没有详细说明API,而是关于推理,背景,幕后和幕后(您是否知道轻松的语义被添加到标准中只是因为POWER和ARM不能有效地支持同步加载?)。
- 3 回答
- 0 关注
- 604 浏览
添加回答
举报