之前的文章里,用罗小猪无缝切换多个女友的故事类比过多线程,并且说为了防止被女友发现可以采用互斥锁的机制来保证线程安全。

但其实除了上锁之外,还有另外一种更高性能的方式也可以保证线程安全,叫做无锁并发。

​谈无锁并发之前,我预设大家都是对多线程有基础的了解的,如果完全不了解的可以先移步这一篇文章《从罗小猪时间管理的角度剖析Linux线程锁》读一读罗小猪的风流韵事,品一品时间管理大师的秘术。

于是我们知道,并发是大型软件不可避免会涉及到的一个问题。你可能要一边运行某个任务,一边还要监听别的任务的指令;或者作为服务端,可能需要为每个连接成功的客户端给予及时的响应,以保证他的用户体验。

在C/C++里,大部分并发需求都会用多线程的方式来实现。

多线程和多进程最主要的区别就是,多进程会为每个进程都分配各自独立的内存空间,而多线程会共享主线程的内存空间。因此进程间通信有信号、管道、共享内存、socket、消息队列等多种多样的手段,而线程间通信就用全局变量或类内成员变量就好了,当然这本质上其实也是共享内存。

既然说到共享,那就一定会出现竞争。当多个线程同时对一块内存进行读写时,势必会出现张冠李戴、错综混乱的情况,因此锁机制应运而生。

我们当然可以用互斥量来为某个代码段加锁,来保证同一时间只有一个线程在操作这块共享区域。不过加锁、竞争锁的开销很大,而且稍有不慎可能会造成死锁,导致程序无法继续运行。

那么是否有一种不加锁的方式来保证并发时共享内存的安全呢?

无锁并发的答案就是原子量atomic。

std::atomic<int> variable;

一旦我们用atomic来修饰某个变量,那么这个变量的操作就不可被打断。即使有很多个线程都可以访问操作这个变量,但由于这个变量的原子性,不可中断的特性,就不会出现某个线程刚操作了一半,却被另一个线程篡改的情况,因此也就实现了并发的安全保证。并且无锁并发的效率,理论上要高于有锁。

不过并不是所有的原子量都是无锁的,可能有的编译器版本底层还是用了互斥量加锁解锁来实现的原子量,所以为了避免折腾半天又跳回了同一条贼船,我们可以通过is_lock_free()函数来查看原子量是否是无锁实现。

​看到这儿的读者是否会有一丝疑惑,既然无锁并发的关键---原子量这么牛叉,为什么还会有有锁并发的存在?

原子量和锁的区别是,基本上原子量能做的,通过加锁也都能做到。但反之则不行,原子量只能保证自己是不被中断的,但无法对代码段的执行提供保护。

这么一比,好像原子量有点弱,难道超过2行的代码,有非原子量的代码,就必须得换成锁了吗?

好在原子量给自己找了个帮手:内存序。

假设这样一种场景:

flag是个普通的全局变量,可被多个线程访问。

线程A里做计算,计算完成后将flag置位;

‍// 计算值data = calculate();//设置flag,通知其他任务值已可用flag = true;

同时线程B监听flag的状态,一旦发现flag被置位,则开始对data做另外的处理。

if(flag){ doSomething(data); }

而编译器这个大聪明,很有可能会自以为是的将flag=true这条语句放到calculate计算之前!

因为它觉得两者似乎是不同的内存、不同的寄存器。它发现calculate在里面计算还得一段时间,它就先把下面的指令也执行了,反正是两个不同的地址。并且它还将这个操作自信的称之为编译器优化。

这样会带来的后果就是,线程B可能会拿到未经过calculate计算的data而进行处理,完全违背了我们设计这段程序的初衷。

用加锁的方式的话,我们可以将这段代码用互斥锁保护起来,保证data计算完成后,flag才会被其他线程拿到;

//线程A
{ mylock.lock(); ‍// 计算值 data = calculate();//设置flag,通知其他任务值已可用flag = true;mylock.unlock();}//线程B{mylock.lock();if(flag){doSomething(data);}mylock.unlock();}

而如果用原子量+内存序的方式的话,则线程A可以这么实现:

//计算值

data = calculate();

//设置flag,通知其他任务值已可用

flag.store(memory_order_release);

如此便可以保证上述代码的正常执行次序,防止被其他线程在flag未设置之前拿到计算值。

即C++原子量提供了load和store两个函数,表示读取和写入。另外提供了内存序,release在写入时使用,表示释放;acquire在读取时使用,表示获取。

store(memory_order_release)会对影响所及的内存区执行一个所谓的release操作,确保此前所有内存操作(all prior memory operations)不论是否为atomic,在store发挥效用之前都变成“可被其他线程看见”。

load(memory_order_acquire)会对影响所及的内存区执行一个所谓的acquire操作,确保随后所有内存操作(all following memory operations)不论是否为atomic,在load之后都变成“可被其他线程看见”。

这里是翻译的C++标准库的原话,这个“可被其他线程看见”的意思就是,其他线程在拿到这个变量时,它的值就是最新的值,或者说对它的操作都已经生效了。

再说的直白一点就是,由于原子量内存序的存在,编译器这个大聪明,不会把flag的置位放到calculate之前了,它会确保store之前的动作,都在store之前发生。而在load之后的动作,都在load之后发生。所以release和acquire经常成对出现,来保证无锁并发时线程之间的同步。

​不知道各位看到这里,有没有想起来volatile这个关键字。

volatile给我们印象是,它是用来防止编译器优化的;是告诉编译器它修饰的变量是易变的,所以每次读取时都从变量内存地址处读取;是应该去修饰那些多任务环境下各任务间共享的标志位的。

乍一看,好像跟上述原子量+内存序有那么一点像,那么它们之间到底有没有区别呢?我得说一声,区别老大了,而且应用的场景可以说是完全不同。

多数教材对volatile的作用解释也有些过时,现代C++里其实不该再用volatile来保证多任务的安全。

篇幅所限,我这里先埋个梗,下一篇文章详细聊聊volatile这个面试高频关键字,和现代C++里的原子量+内存序的区别和联系,彻底搞定这个知识点。

C++原子量,内存序,无锁并发相关推荐

  1. 【共享内存】基于共享内存的无锁消息队列设计

    上交所技术服务 2018-09-05 https://mp.weixin.qq.com/s/RqHsX3NIZ4_BS8O30KWYhQ 目录 一.背景 二.消息队列的应用需求 (一)  通信架构的升 ...

  2. Java无锁并发详细教程

    问题提出 有如下需求,保证 account.withdraw 取款方法的线程安全 package cn.itcast;import java.util.ArrayList; import java.u ...

  3. 无锁并发的CAS为何如此优秀?

    Talk is cheap CAS(Compare And Swap),即比较并交换.是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数--内存位置(V).预期原值(A)和新 ...

  4. 【java并发编程】无锁并发框架disruptor

    一.简介 Disruptor是一个高性能队列,研发的初衷是解决内部的内存队列的延迟问题,而不是分布式队列.基于Disruptor开发的系统单线程能支撑每秒600万订单. 使用场景:对延时要求很高的场景 ...

  5. java 无锁框架_高性能无锁并发框架 Disruptor,太强了!

    Java技术栈 www.javastack.cn 关注优质文章 Disruptor是一个开源框架,研发的初衷是为了解决高并发下队列锁的问题,最早由LMAX提出并使用,能够在无锁的情况下实现队列的并发操 ...

  6. Java并发编程,无锁CAS与Unsafe类及其并发包Atomic

    为什么80%的码农都做不了架构师?>>>    我们曾经详谈过有锁并发的典型代表synchronized关键字,通过该关键字可以控制并发执行过程中有且只有一个线程可以访问共享资源,其 ...

  7. Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772470 出自[zejian ...

  8. 学习笔记:Java 并发编程④_无锁

    若文章内容或图片失效,请留言反馈. 部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 视频链接:https://www.bilibili.com/video/av81461839 配套资料: ...

  9. JUC并发编程共享模型之无锁(五)

    5.1 问题引出 public interface Account {// 获取余额Integer getBalance();void withdraw(Integer amount);/*** 方法 ...

  10. 【C++】多线程与原子操作和无锁编程【五】

    [C++]多线程与原子操作和无锁编程[五] 1.何为原子操作 前面介绍了多线程间是通过互斥锁与条件变量来保证共享数据的同步的,互斥锁主要是针对过程加锁来实现对共享资源的排他性访问.很多时候,对共享资源 ...

最新文章

  1. android的天气和时钟部件,Android的天气和时钟部件
  2. 【Android】Android取消EditText自动获取焦点
  3. 2021春季学期-创新设计与实践-Lesson3
  4. [转]MongoDB c++驱动安装与使用
  5. 互联网晚报 | 12月28日 星期二 | 百度首次在元宇宙开会;快手美团达成互联互通合作;新能源车险交易平台正式上线...
  6. 解读clickhouse存算分离在华为云实践
  7. Android学习导航线路
  8. c语言中联机停止运行,Win7开机提示Microsoft(C)注册服务器已停止工作怎么办?
  9. Hadoop学习笔记(一):MapReduce工作机制
  10. 测试过程中常用的linux命令之【删除指定的文件行】
  11. 修改文件类型(txt文件改为bat、sh文件)
  12. 二/三维空间曲面的切平面以及在某一点上的切线,法线
  13. 宝宝泡药浴和直接吃药有什么区别吗?
  14. TEACH_NLP——开篇
  15. MR案例(1)词频统计
  16. python 节点关系图_在Python中如何分析和识别有向图关系(节点间)
  17. 国家教育部牵手曙光公司——“百校工程”助力教育行业大数据平台建设
  18. 《大道至简》第二章读后感--是懒人造就了方法
  19. 京东商品发布实现店铺商品批量上新,节省大量人力
  20. Fastadmin后台登入问题

热门文章

  1. matlab基因频率是看最大值吗,基于ICA的语音信号盲分离
  2. 这篇文章告诉你PDF转WORD免费软件有哪些?
  3. 海量数据处理分析经验和技巧
  4. Oracle-Oracle数据库备份与恢复
  5. serialVersionUID 问题处理
  6. java 启动resin,Resin 启动时报错!解决方法
  7. java 金额转大写人民币
  8. easyui简单登陆界面
  9. kali攻击手机_kali linux入侵安卓手机
  10. [bzoj1406][数论]密码箱