C++ memory order循序渐进(一)—— 多核编程中的lock free和memory model_c++ 多核编程-程序员宅基地

技术标签: c++  java  [软件开发]C/C++  开发语言  


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

1. 多核编程面临的问题

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

如何进行各个核心之间的数据同步是关键问题,CPU层面,为了追求高性能,使用的是一套包括MESI协议、store buffer、invalid queue等技术在内的数据同步方式,数据在各个核之间的一致性和可见性并不是那么理想,再加上编译器也会做优化,最终观察到的现象就是编译和运行的时候都会有重排的情况,也就是内存数据的同步和代码顺序不一致,当然,cpu和编译器也是有底线的,无论产生什么样的重排,都会保证对于单线程内部的执行结果不会有任何区别,但是进入多核时代后,原本人畜无害的重排可能会出现大问题,一个简单的例子如下:
在这里插入图片描述

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

2. lock free

2.1 lock free的定义

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

维基百科上的定义如下:
Lock free允许单独的线程个体阻塞,但是会保证系统整体上的吞吐,如果一个算法对应的程序的线程在运行了足够长时间的情况下,至少有一个线程取得了进展,那么我们说这个算法是lock-free的。

概括起来就是,如果涉及到共享内存的多线程代码在多线程执行下不可能互相影响导致被hang住,不管OS如何调度线程,至少有一个线程在做有用的事,那么就是lock-free。使用了锁的代码肯定不是lock free,因为一个线程加锁后如果被系统切出去了其他所有线程都处于等待中。但是没用锁也不一定是lock free,因为普通的代码逻辑也可能会导致一个线程hang住另一个线程。锁之所以在高并发的时候表现很差,主要原因就是加锁的线程会hang住其他待加锁的线程,lock-free可以很好的解决这一问题。

需要强调的一点是,并不是说lock-free的算法就一定比加锁的算法好,lock-free需要处理更多更复杂的race condition和ABA等问题,编写出合理的lock-free代码也需要更深厚的功底,需要对底层有更多地了解,完成相同目的的代码会比用锁更复杂,执行时间可能更长,代码也更难理解。很多场景合理地使用锁就能很好的胜任,lock-free和锁之间在应用场景上更多的是一种互补的关系。lock-free算法的价值在于其保证了一个或所有线程始终在做有用的事,而不是绝对的高性能。但lock-free相较于锁在并发度高(竞争激烈导致上下文切换开销变得突出)的某些场景下会有很大的性能优势,比如实现一个多线程的lock-free queue,总的来说,在多核环境下,lock-free是很有意义的。

2.2 Lock free 相关技术

Lock free 编程既然要抛弃传统意义上的锁,那么往更底层走自然成为必然,实现lock free最重要的相关技术就是原子操作控制memory order

作为程序员或多或少对原子操作都有一定概念,cpu保证没有线程能观察到原子操作的中间态,也就是对于一个原子操作,对于所有的线程来说要么做了要么没做,对于现代cpu,很多操作本身就是原子的。原子操作主要包括赋值原子操作、Read-Modify-Write(比如c++11里的fetch_add)、Compare-And-Swap(比如c++11里的compare_exchange_strong)。原子操作保证了各线程在进行共享内存的存取的时候能读到完整的值。

而memory order,字面意思是内存序,这是一个比较笼统的概念,对于cpu来说,执行代码对内存的操作就形成了memory order,而这个order受编译器和cpu运行时重排的影响,当然这两种重排是有方法可以限制的。因为lock free算法没有显式的锁,将会直接观察到这些和代码顺序不一致的重排,给lock free算法的实现带来了挑战。c++ 11后也正式引入了memory order的概念,c++的memory oder更多则是一种约定和方法,给程序员提供了一种跨平台的通用方法来限制上述两种重排,这也极大地方便了我们实现lock free算法,在此之前,程序员如果想要限制重排,可能需要手动调用不同平台的专有fence。

关于程序运行时的memory order,重点提一下sequential consistency,假设没有重排,那么就是sequential consistency的内存序,也就是顺序一致,所有的线程都能看到一样的内存修改顺序,同时这个顺序和程序的源码是一致的,也就是各线程的内存操作是按顺序交织的,这种memory order下,程序的行为很好预期,但目前没有任何主流cpu提供sequential consistency保证,也就是都可能会进行不同程度的指令重排,最终会出现什么样的memory order,则是由对应的memory model决定的。

3. Memory model

编译器在编译时和cpu在运行时都可能对代码和指令进行重排,导致出现和我们编写的代码不一致的情况,但是这些重排必须遵循某些规则,否则就乱套了,而Memory model,内存模型,就定义了特定处理器上或者工具链上的重排情况,换句话说,某个处理器或者工具链对于代码的重排会严格遵循对应的memory model,什么情况可以重排、什么情况不能重排。

3.1 reorder类型和Memory model的强弱

对内存的操作可以概括为读和写,也就是load和store,因此reorder也就可以整体上分为以下四种类型:

  1. Loadload reorder:两个读操作之间重排
  2. Loadstore reorder:原来在写操作之前的读操作重排到之后
  3. Storeload reorder:原来在读操作之前的写操作重排到之后
  4. Storestore reorder:两个写操作之间重排

这几种重排在不同的平台上是否会出现也决定了平台对应的memory model是强是弱,memory model既有软件层面的software memory model,又有硬件平台的hardware memory model,这里再引用一张preshing博客里的图,图中涵盖了典型的memory order,从左到右越来越强。总的来说memory model按照严格程度可以分为strong和weak两种,然后根据强弱程度的不同又可以再细分。
在这里插入图片描述

图中的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,顾名思义,编译器层面的屏障,可以防止编译器在将源码转换成机器码的过程中重排,一个例子如下:
在这里插入图片描述
对于以上代码,用GCC4.6.1整体不开启优化进行编译得到的汇编代码如下:
在这里插入图片描述

可以看到没有重排,因为重排的目的就是优化。开启优化后如下:
在这里插入图片描述

出现了重排,如果想要整体开启优化,但是对于这部分代码不想要重排,那么就可以使用Compiler Barrier,在GCC里,asm volatile(“” ::: “memory”)就是这么一个Compiler Barrier。对于以下代码:
在这里插入图片描述

开启优化编译后如下,和最初的一致:
在这里插入图片描述

上面说的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,定义了一个宏:

#define RELEASE_FENCE() asm volatile(“lwsync” ::: “memory”)

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/heli200482128/article/details/113779463

智能推荐

5个超厉害的资源搜索网站,每一款都可以让你的资源满满!_最全资源搜索引擎-程序员宅基地

文章浏览阅读1.6w次,点赞8次,收藏41次。生活中我们无时不刻不都要在网站搜索资源,但就是缺少一个趁手的资源搜索网站,如果有一个比较好的资源搜索网站可以帮助我们节省一大半时间!今天小编在这里为大家分享5款超厉害的资源搜索网站,每一款都可以让你的资源丰富精彩!网盘传奇一款最有效的网盘资源搜索网站你还在为找网站里面的资源而烦恼找不到什么合适的工具而烦恼吗?这款网站传奇网站汇聚了4853w个资源,并且它每一天都会持续更新资源;..._最全资源搜索引擎

Book类的设计(Java)_6-1 book类的设计java-程序员宅基地

文章浏览阅读4.5k次,点赞5次,收藏18次。阅读测试程序,设计一个Book类。函数接口定义:class Book{}该类有 四个私有属性 分别是 书籍名称、 价格、 作者、 出版年份,以及相应的set 与get方法;该类有一个含有四个参数的构造方法,这四个参数依次是 书籍名称、 价格、 作者、 出版年份 。裁判测试程序样例:import java.util.*;public class Main { public static void main(String[] args) { List <Book>_6-1 book类的设计java

基于微信小程序的校园导航小程序设计与实现_校园导航微信小程序系统的设计与实现-程序员宅基地

文章浏览阅读613次,点赞28次,收藏27次。相比于以前的传统手工管理方式,智能化的管理方式可以大幅降低学校的运营人员成本,实现了校园导航的标准化、制度化、程序化的管理,有效地防止了校园导航的随意管理,提高了信息的处理速度和精确度,能够及时、准确地查询和修正建筑速看等信息。课题主要采用微信小程序、SpringBoot架构技术,前端以小程序页面呈现给学生,结合后台java语言使页面更加完善,后台使用MySQL数据库进行数据存储。微信小程序主要包括学生信息、校园简介、建筑速看、系统信息等功能,从而实现智能化的管理方式,提高工作效率。

有状态和无状态登录

传统上用户登陆状态会以 Session 的形式保存在服务器上,而 Session ID 则保存在前端的 Cookie 中;而使用 JWT 以后,用户的认证信息将会以 Token 的形式保存在前端,服务器不需要保存任何的用户状态,这也就是为什么 JWT 被称为无状态登陆的原因,无状态登陆最大的优势就是完美支持分布式部署,可以使用一个 Token 发送给不同的服务器,而所有的服务器都会返回同样的结果。有状态和无状态最大的区别就是服务端会不会保存客户端的信息。

九大角度全方位对比Android、iOS开发_ios 开发角度-程序员宅基地

文章浏览阅读784次。发表于10小时前| 2674次阅读| 来源TechCrunch| 19 条评论| 作者Jon EvansiOSAndroid应用开发产品编程语言JavaObjective-C摘要:即便Android市场份额已经超过80%,对于开发者来说,使用哪一个平台做开发仍然很难选择。本文从开发环境、配置、UX设计、语言、API、网络、分享、碎片化、发布等九个方面把Android和iOS_ios 开发角度

搜索引擎的发展历史

搜索引擎的发展历史可以追溯到20世纪90年代初,随着互联网的快速发展和信息量的急剧增加,人们开始感受到了获取和管理信息的挑战。这些阶段展示了搜索引擎在技术和商业模式上的不断演进,以满足用户对信息获取的不断增长的需求。

随便推点

控制对象的特性_控制对象特性-程序员宅基地

文章浏览阅读990次。对象特性是指控制对象的输出参数和输入参数之间的相互作用规律。放大系数K描述控制对象特性的静态特性参数。它的意义是:输出量的变化量和输入量的变化量之比。时间常数T当输入量发生变化后,所引起输出量变化的快慢。(动态参数) ..._控制对象特性

FRP搭建内网穿透(亲测有效)_locyanfrp-程序员宅基地

文章浏览阅读5.7w次,点赞50次,收藏276次。FRP搭建内网穿透1.概述:frp可以通过有公网IP的的服务器将内网的主机暴露给互联网,从而实现通过外网能直接访问到内网主机;frp有服务端和客户端,服务端需要装在有公网ip的服务器上,客户端装在内网主机上。2.简单的图解:3.准备工作:1.一个域名(www.test.xyz)2.一台有公网IP的服务器(阿里云、腾讯云等都行)3.一台内网主机4.下载frp,选择适合的版本下载解压如下:我这里服务器端和客户端都放在了/usr/local/frp/目录下4.执行命令# 服务器端给执_locyanfrp

UVA 12534 - Binary Matrix 2 (网络流‘最小费用最大流’ZKW)_uva12534-程序员宅基地

文章浏览阅读687次。题目:http://acm.hust.edu.cn/vjudge/contest/view.action?cid=93745#problem/A题意:给出r*c的01矩阵,可以翻转格子使得0表成1,1变成0,求出最小的步数使得每一行中1的个数相等,每一列中1的个数相等。思路:网络流。容量可以保证每一行和每一列的1的个数相等,费用可以算出最小步数。行向列建边,如果该格子是_uva12534

免费SSL证书_csdn alphassl免费申请-程序员宅基地

文章浏览阅读504次。1、Let's Encrypt 90天,支持泛域名2、Buypass:https://www.buypass.com/ssl/resources/go-ssl-technical-specification6个月,单域名3、AlwaysOnSLL:https://alwaysonssl.com/ 1年,单域名 可参考蜗牛(wn789)4、TrustAsia5、Alpha..._csdn alphassl免费申请

测试算法的性能(以选择排序为例)_算法性能测试-程序员宅基地

文章浏览阅读1.6k次。测试算法的性能 很多时候我们需要对算法的性能进行测试,最简单的方式是看算法在特定的数据集上的执行时间,简单的测试算法性能的函数实现见testSort()。【思想】:用clock_t计算某排序算法所需的时间,(endTime - startTime)/ CLOCKS_PER_SEC来表示执行了多少秒。【关于宏CLOCKS_PER_SEC】:以下摘自百度百科,“CLOCKS_PE_算法性能测试

Lane Detection_lanedetectionlite-程序员宅基地

文章浏览阅读1.2k次。fromhttps://towardsdatascience.com/finding-lane-lines-simple-pipeline-for-lane-detection-d02b62e7572bIdentifying lanes of the road is very common task that human driver performs. This is important ..._lanedetectionlite

推荐文章

热门文章

相关标签