原文:
WilburDing's Blog,http://wilburding.github.io/blog/2013/04/07/c-plus-plus-11-atomic-and-memory-model/
C++11已经出来好久了,最近才刚开始研究。。
我们知道,C++一般尽量使用库提供功能特性而不是从语言本身开刀(貌似python也是这么宣称的),但这次C++11标准带来了大量的语言特性,甚至可以当成一门新语言来学,哪怕你有C++基础,这其中包括重在提高运行时性能的右值引用,提高易用性的统一初始化、lambda、自动类型推断等,增强语言功能的变参模版、用户自定义字面常量等。随着现在多核的流行,C++终于在语言层面上支持了多线程编程,提供了线程类、同步原语、原子类型以及异步相关的类型和操作。你要问为什么要在语言层面上进行支持而不是只用库提供,毕竟现在已经有大量用C++加类库写成的多线程代码了,而且似乎都工作的很好,因为 Threads Cannot be Implemented as a Library.
Memory Order
查看STL提供的原子操作类型Atomic的接口(std::atomic)可以发现,几乎每个方法都有一个类型为memory_order的默认参数,默认值是std::memory_order_seq_cst。再翻翻,发现memory_order是个枚举类型,并且还有其他几个值memory_order_relaxed/memory_order_consume/memory_order_acquire/memory_order_release/memory_order_acq_rel,这是什么东东?看下边的解释貌似(memory_order)也不知道在说什么,于是猫就被害死了。。
Memory Model
Memory order具体是干嘛的呢,这要从memory model说起。不过memory model貌似有很多意思,比如我们知道的x86 cpu的段式存储就是一种memory model. 但是这里要说的是指线程间通过内存的交互以及它们之间共享数据的使用(wiki),这个主要与编译器有关了。
编译器从计算机的远古时代发展至今,已经不是简单的把我们写的代码一股脑翻译成机器码了,而是会进行非常多的优化,有的优化是单纯将我们写的糟糕代码换成更有效的代码,比如将循环内不变量提出,有的则是为了充分利用现代CPU的特性(乱序执行神马的),这种优化在普通人看来就很费解,比如我们写了
1 2 |
|
编译器可能就会转换成‘
1 2 |
|
如果跟踪过-O2或者更高优化级别的代码运行肯定会深有体会,代码的执行不再是按照我们写的顺序来,而是忽上忽下的,查看变量时调试器还经常告诉你变量已经被优化掉了。至于为什么要调换代码的执行顺序,可能是编译器觉得现代CPU的store操作比load操作代价大,所以先执行load再来store吧。
优化虽好,可不是没有坏处。当然不好调试算一个,但致命问题是,这些优化都是建立在程序是在一个线程中执行的。调整语句顺序也好,调整嵌套循环的嵌套关系也好,还是把循环用变量一直放到寄存器直到循环结束才写回内存,编译器都会保证不会破坏程序的语义(当然不排除聪明过头的情况),使得优化后的程序执行结果和程序员原始代码一致。一旦多线程运行,平时默默无闻的优化就露出了马脚,比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
假如f1和f2各在一个线程中运行,f2能打印出来sum的计算过程吗,即使不是完全的?答案只能是不一定,如果f1执行时sum一直在寄存器里,那f2只能看到f1执行完后的sum,中间过程根本看不到。那么为了多线程程序的正确执行,是不是只要禁了编译器的优化就可以了呢?答案是不能,就算能,你愿意接受运行缓慢的程序,那编译器作者也不乐意,那么多年心血就付诸东流了,CPU也更欲哭无泪。
为什么禁了编译器优化也不能正确运行呢,因为。。。现代CPU速度非常快,频率都在2GHz上下甚至3GHz还多,以前Pentium4都有超频到8GHz左右的,按2GHz算,一个周期就是0.5ns;而它的搭档内存又“非常慢”,核心频率大概在200MHz上下(神马,我的DDR3不是都1600Mhz吗,额,那是数据传输频率,大概就是核心频率一下可以吐8个bit而不是1个,换算回来核心频率还是200MHz,单论反应速度不比当年的SDRAM好多少),然后还有反应延迟,CPU说要某个地址的数据了,内存要慢悠悠转上几个周期先,然后才能拿到数据往CPU送,所以内存引用一次就要消耗大概100ns。CPU执行计算肯定要使用内存的数据,要是这么等内存,那太伤CPU感情了,白跑那么快,全浪费在等内存上了,所以根据程序的局部性原理(Locality of Reference),在CPU和内存之间加上一个比内存小点但快很多的cache来保存可能要用到的数据,然后我们就获得了一个比cache略慢但是和主内存一样大的内存;内存也不甘寂寞,通过pipeline的方式使自己忙起来,尽力一次多传点数据。有了这些机制也不意味着CPU就能直接从中完全受益,还需要CPU进行很多的配合以充分利用,比如一旦cache命中失败,CPU可以执行后边的指令而不是干等,这样就有了乱序执行,这种内存操作的性质称为Memory Ordering.而且有了cache,CPU往内存写数据也要多考虑点,是直接连cache带内存都写呢还是先写cache一会再写到内存呢,如果其他cpu的cache里也有相同数据怎么办呢。这样,当多个线程同时执行时,彼此都可以看到对方的执行样子,这么赤裸裸的,怎么会不出问题呢。
到了这份田地,正确的多线程程序该怎么写呢?于是编译器说,我知道你的程序在单线程里的情况,但是我不知道哪些数据是线程间共享的,所以,你来告诉我吧!这样,我可以在必要的时候保守的优化,一般情况下全力优化,并且只要你保证你的程序是正确同步的,那我保证程序执行时就是你要的那个样子。这样一个在程序员和语言间的约定,就是Memory Model。
Ordering
刚开始提到的memory_order那几个枚举就是告知编译器数据是在线程间共享的,并且是怎样共享的。memory_order_acquire和memory_order_release的关系大概就像mutex的acquire(lock)和release(unlock)。mutex使线程间串行执行,并且上一个线程unlock后另一个线程lock进入,保证可以看到上一个线程的所有数据修改(或曰side effect)而不会有读写操作被reorder后出问题的情况。同样的,一个atomic a变量,一个线程t1 release(一般是store操作, a.store(memory_order_release))后,另一个线程t2 acquire(一般是load操作, a.load(memory_order_acquire)),那t2可以看到上个release线程的所有修改(不管是不是atomic的变量),但这不是说a.load的时候会一直等另一个线程的a.store,他们只是在执行时间上有个happen-before的关系,即t1的a.store发生在t2的a.load之前,如果他们同时发生,那就说明你的代码没有正确同步,只能自求多福了。比如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
thread_2通过不断循环保证自己最后读到flag不为0时发生在t1的flag.store之后(因果关系)。从这里也可以看出,atomic的acquire/release时更多的是在起同步作用。
为了达到acquire/release的要求,编译器会对这样的操作比较小心。release时,编译器保证不会把写操作移到它后边,否则其他线程就看不到这个修改了;acquire时,不会把其后的读操作移到它前面,否则读到的就是旧数据。剩下的情况,编译器就可以自由移动读写操作了。除了编译器,有时也需要CPU的配合,因为它执行的时候也会reorder指令,所幸的是x86/x64平台上,mov指令是具有acquire/release语义的,所以我们基本没有什么性能损失。
除此之外还有几个其他的memory_order枚举。
- memory_order_relaxed说明这个操作除了是原子的外,周围的操作随便移动,比较适合做计数器。
- memory_order_acq_rel基本是memory_order_acquire和memory_order_release的合体。
- memory_order_scq_cst是memory_order_acq_rel的加强版,除了有acq_rel语义,还保证是sequencially-consistent(人家Lamport同学70年代就在研究这个了。。)的,这是atomic各种操作的默认memory_order,也是执行代价最大的。选memory_order_scq_cst做默认参数是因为它是最容易用的memory_order,并且性能要比用mutex好点,如果性能的确成为瓶颈可以换其他memory_order慢慢调优,具体可以看这个paper。
- memory_order_consume是memory_order_acquire的弱化版,它只保证不把跟当前load的变量有依赖的变量reorder,没依赖关系的随便移动。比如上边的代码中,data对flag没有依赖,所以thread_2里flag.load换成memory_order_consume后,读到的data就不能保证是1了。但是经常有一些情况是比较合适的,比如通过指针传递一些数据的情况。
当然,你知道了这些memory_order后,也不一定意味着能用它们写出来正确的代码,因为你在reason代码的时候基本是在考虑各种代码reorder的排列组合,当然也可能是我比较stupid的。。但是人家Herb Sutter至少把memory_order_consume列成了C++语言的专家级特性呢。。
Finally
因为我并没有太多的多线程并发开发经验,所以不知道以上有多少是在胡扯,不过我尽力保证是正确的,如果哪位发现有不对的地方,还请多谢指正。。
最后推荐下Herb Sutter的这个talk,Atomic Weapons 1 Atomic Weapon 2,这篇blog大多也是参考这个talk的slides写的。
相关推荐
内容概要:本文详细介绍了基于MATLAB GUI界面和卷积神经网络(CNN)的模糊车牌识别系统。该系统旨在解决现实中车牌因模糊不清导致识别困难的问题。文中阐述了整个流程的关键步骤,包括图像的模糊还原、灰度化、阈值化、边缘检测、孔洞填充、形态学操作、滤波操作、车牌定位、字符分割以及最终的字符识别。通过使用维纳滤波或最小二乘法约束滤波进行模糊还原,再利用CNN的强大特征提取能力完成字符分类。此外,还特别强调了MATLAB GUI界面的设计,使得用户能直观便捷地操作整个系统。 适合人群:对图像处理和深度学习感兴趣的科研人员、高校学生及从事相关领域的工程师。 使用场景及目标:适用于交通管理、智能停车场等领域,用于提升车牌识别的准确性和效率,特别是在面对模糊车牌时的表现。 其他说明:文中提供了部分关键代码片段作为参考,并对实验结果进行了详细的分析,展示了系统在不同环境下的表现情况及其潜在的应用前景。
嵌入式八股文面试题库资料知识宝典-计算机专业试题.zip
嵌入式八股文面试题库资料知识宝典-C and C++ normal interview_3.zip
内容概要:本文深入探讨了一款额定功率为4kW的开关磁阻电机,详细介绍了其性能参数如额定功率、转速、效率、输出转矩和脉动率等。同时,文章还展示了利用RMxprt、Maxwell 2D和3D模型对该电机进行仿真的方法和技术,通过外电路分析进一步研究其电气性能和动态响应特性。最后,文章提供了基于RMxprt模型的MATLAB仿真代码示例,帮助读者理解电机的工作原理及其性能特点。 适合人群:从事电机设计、工业自动化领域的工程师和技术人员,尤其是对开关磁阻电机感兴趣的科研工作者。 使用场景及目标:适用于希望深入了解开关磁阻电机特性和建模技术的研究人员,在新产品开发或现有产品改进时作为参考资料。 其他说明:文中提供的代码示例仅用于演示目的,实际操作时需根据所用软件的具体情况进行适当修改。
少儿编程scratch项目源代码文件案例素材-剑客冲刺.zip
少儿编程scratch项目源代码文件案例素材-几何冲刺 转瞬即逝.zip
内容概要:本文详细介绍了基于PID控制器的四象限直流电机速度驱动控制系统仿真模型及其永磁直流电机(PMDC)转速控制模型。首先阐述了PID控制器的工作原理,即通过对系统误差的比例、积分和微分运算来调整电机的驱动信号,从而实现转速的精确控制。接着讨论了如何利用PID控制器使有刷PMDC电机在四个象限中精确跟踪参考速度,并展示了仿真模型在应对快速负载扰动时的有效性和稳定性。最后,提供了Simulink仿真模型和详细的Word模型说明文档,帮助读者理解和调整PID控制器参数,以达到最佳控制效果。 适合人群:从事电力电子与电机控制领域的研究人员和技术人员,尤其是对四象限直流电机速度驱动控制系统感兴趣的读者。 使用场景及目标:适用于需要深入了解和掌握四象限直流电机速度驱动控制系统设计与实现的研究人员和技术人员。目标是在实际项目中能够运用PID控制器实现电机转速的精确控制,并提高系统的稳定性和抗干扰能力。 其他说明:文中引用了多篇相关领域的权威文献,确保了理论依据的可靠性和实用性。此外,提供的Simulink模型和Word文档有助于读者更好地理解和实践所介绍的内容。
嵌入式八股文面试题库资料知识宝典-2013年海康威视校园招聘嵌入式开发笔试题.zip
少儿编程scratch项目源代码文件案例素材-驾驶通关.zip
小区开放对周边道路通行能力影响的研究.pdf
内容概要:本文探讨了冷链物流车辆路径优化问题,特别是如何通过NSGA-2遗传算法和软硬时间窗策略来实现高效、环保和高客户满意度的路径规划。文中介绍了冷链物流的特点及其重要性,提出了软时间窗概念,允许一定的配送时间弹性,同时考虑碳排放成本,以达到绿色物流的目的。此外,还讨论了如何将客户满意度作为路径优化的重要评价标准之一。最后,通过一段简化的Python代码展示了遗传算法的应用。 适合人群:从事物流管理、冷链物流运营的专业人士,以及对遗传算法和路径优化感兴趣的科研人员和技术开发者。 使用场景及目标:适用于冷链物流企业,旨在优化配送路线,降低运营成本,减少碳排放,提升客户满意度。目标是帮助企业实现绿色、高效的物流配送系统。 其他说明:文中提供的代码仅为示意,实际应用需根据具体情况调整参数设置和模型构建。
少儿编程scratch项目源代码文件案例素材-恐怖矿井.zip
内容概要:本文详细介绍了基于STM32F030的无刷电机控制方案,重点在于高压FOC(磁场定向控制)技术和滑膜无感FOC的应用。该方案实现了过载、过欠压、堵转等多种保护机制,并提供了完整的源码、原理图和PCB设计。文中展示了关键代码片段,如滑膜观测器和电流环处理,以及保护机制的具体实现方法。此外,还提到了方案的移植要点和实际测试效果,确保系统的稳定性和高效性。 适合人群:嵌入式系统开发者、电机控制系统工程师、硬件工程师。 使用场景及目标:适用于需要高性能无刷电机控制的应用场景,如工业自动化设备、无人机、电动工具等。目标是提供一种成熟的、经过验证的无刷电机控制方案,帮助开发者快速实现并优化电机控制性能。 其他说明:提供的资料包括详细的原理图、PCB设计文件、源码及测试视频,方便开发者进行学习和应用。
基于有限体积法Godunov格式的管道泄漏检测模型研究.pdf
嵌入式八股文面试题库资料知识宝典-CC++笔试题-深圳有为(2019.2.28)1.zip
少儿编程scratch项目源代码文件案例素材-几何冲刺 V1.5.zip
Android系统开发_Linux内核配置_USB-HID设备模拟_通过root权限将Android设备转换为全功能USB键盘的项目实现_该项目需要内核支持configFS文件系统
C# WPF - LiveCharts Project
少儿编程scratch项目源代码文件案例素材-恐怖叉子 动画.zip
嵌入式八股文面试题库资料知识宝典-嵌⼊式⼯程师⾯试⾼频问题.zip