C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model

文章目录

  • 1. 多核编程面临的问题
  • 2. lock free
    • 2.1 lock free的定义
    • 2.2 Lock free 相关技术
  • 3. Memory model
    • 3.1 reorder类型和Memory model的强弱
    • 3.2 Compiler Barrier和Runtime Memory Barrier
  • 4. c++ 11 memory order

前面看brpc的源码的时候发现很多地方为了追求性能很多地方都指定了memory_order,于是也去专门学习了一阵,同时准备写博客记录下,原本觉得一两篇就够了,但是一路看过来感觉要讲明白一两篇着实不够,所以打算写一个系列,循序渐进地聊一聊C++ 11标准正式引入的memory order,这是第一篇,算是个引子吧,主要是聊聊memory order相关的lock free和memory model,方便引出c++ memory order,如有不准确的地方欢迎大家指正。

1. 多核编程面临的问题

在cpu的发展史上,早期的性能提升主要来自于主频和架构的提升,在这条道路出现瓶颈后,多核cpu开始流行起来,到目前为止,从桌面cpu到各类移动设备cpu,多核心几乎已经成为标配,很显然,多核心可以同时执行能极大提高性能,但也对硬件架构和软件编写提出了更大的挑战,各核心都有自己的cache,还有不同层级的cache,彼此共享内存,一个典型的多核cpu架构如下:

C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model

对于Thread1内部,p和ready没有关联,有可能重排,而Thread2依赖ready做标识位,一旦重排,Thread2在看到ready=true的时候p都可能没有init,显然这是有问题的。所幸的是,各种对并行编程支持较好的高级语言发展到现在都有许多成熟的工具能够让我们方便地进行多核编程不用关心底层的这些东西,最典型的就是封装完善的锁,接触过多线程编程的程序员对锁肯定不会陌生,锁很好理解,用起来也很方便,能够让程序员在不需要知道太多底层细节地情况下写出正确的多核环境适用的代码,但是在并发很高的时候锁竞争很容易成为瓶颈,因此在进行多核编程的某些场景下锁并不能满足要求。

2. lock free

2.1 lock free的定义

程序员总是追求极致的,既然锁在在高并发的时候很容易成为瓶颈,我们需要其他的手段来进行代价更小的数据同步,在各路大神的引领下,Lock free的概念应运而生并逐渐流行起来。Lock free programing,字面意思就是无锁编程,很多人的理解是成没有用到各类显式锁的编程,这个理解并不准确,这个概念有着不同的表述方式,这里先贴一个preshing大佬博客里通俗易懂的图:

C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model

图中的DEC Alpha,也是被各种文章屡屡提起的明星示例,四种reorder都有可能发生,只要不改变单线程内部的执行正确性。

ARM架构的cpu则是在此基础上额外保证了数据依赖顺序。

至于X86/X64这种就属于强memory model的范畴了,此类平台只可能发生Storeload reorder。

上面说的这几种都是cpu平台的hardware memory model,对于上面提到过的最强的Sequential Consistency,目前的主流cpu都不会在硬件层面上直接强制提供此类保证,因为代价太大,很多时候也没有必要。但是如果确实有需要可以在软件层面进行相关限制让cpu实现,像图里的C++ 11的原子操作的默认内存序属于software memory model的范畴,就可以实现顺序一致的效果。

3.2 Compiler Barrier和Runtime Memory Barrier

无论是哪种memory model,hardware memory model还是 software memory model,上面说到的这些可能的重排都是指的在没有其他限制的情况,为了能够保证程序的正确性,CPU和编译器(语言)的设计者都给我们留了手段来改变这些重排,这类手段可以抽象成一个统一的概念barrier,屏障。无论上面提到的那些memory model是定义在哪个层面的,要想限制重排,具体到最后的程序的编写、编译、运行步骤,总是需要程序员用代码来限制编译阶段和运行阶段的重排,因此可以分为Compiler Barrier和runtime memory barrier。

Compiler Barrier,顾名思义,编译器层面的屏障,可以防止编译器在将源码转换成机器码的过程中重排,一个例子如下:

C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model

可以看到没有重排,因为重排的目的就是优化。开启优化后如下:

C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model

开启优化编译后如下,和最初的一致:

C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model

上面说的asm volatile(“” ::: “memory”)是显式的Compiler Barrier,它只能保证编译阶段不重排,在多核系统里,光做到这一点还不够,因为它没法对cpu核心运行时的重排做出限制。因此,在多核编程中,更多的时候我们需要同时对编译重排和运行时重排做出限制,所以我们还需要 run time memory barrier。

runtime memory barrier是限制运行时重排的手段,通常是通过特定的CPU 指令来实现,上面提到了重排基本可以分为四种类型,因此对应的barrier也有四种

  1. Loadload barrier:保证barrier前的读操作比barrier后的读操作先完成。
  2. Loadstore barrier:保证barrier前的读操作比barrier后的写操作先完成。
  3. Storeload barrier:保证barrier前的写操作比barrier后的读操作先完成。
  4. Storestore barrier:保证barrier前的写操作比barrier后的写操作先完成。

还是以GCC为例,GCC对于PowerPC,定义了一个宏:

lwsync是PowerPC平台的一个cpu指令,可以限制运行时重排,它同时具有LoadLoad barrier, LoadStore barrier StoreStore barrier的效果。具有runtime memory barrier的效果,而且需要重点注意的是,一旦在代码里插入了RELEASE_FENCE(),除了会插入一条lwsync指令限制运行时重排之外,它也会阻止编译器编译阶段重排,也就是说,RELEASE_FENCE()不光是runtime memory barrier ,也是隐式的Compiler Barrier。

4. c++ 11 memory order

Memory order概念的引入,可以说是c++ 11标准最重要的特性之一了。在此之前,标准C++甚至连thread都没有,自然也就没有像现在的memory order这种相对便利的控制内存序的机制,在各种各样的不同的平台面前,要用c++实现lock free代码没那么容易,更别说跨平台了,不同的平台底层控制内存序的指令之类的都不尽相同,c++引入了标准的memory order后将最底层的细节隐藏了起来,程序员只需要关注c++ memory order这个软件层面的memory order,可以理解为语言和编译器层面定了一套通用协议,程序员按照协议规定的方式编写代码就能得到想要的内存序,而不用关心是什么编译器编译、在什么平台运行。有点像后端服务调用下游接口,只需要提供对方定义好的一些参数,对方根据你的参数返回对应的结果,你不用关心下游接口运行在哪、内部是怎么得到这些结果的。

后续将会针对一步步c++ 11的memory order展开详细介绍,希望能有时间保持更新频率。

参考:
https://preshing.com
https://github.com/apache/incubator-brpc/blob/master/docs/en/atomic_instructions.md

来源:贺二公子

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年1月8日
下一篇 2021年1月8日

相关推荐