利用C++11原子量atomic实现自旋锁详解
一、自旋锁
自旋锁是一种基础的同步原语,用于保障对共享数据的互斥访问。与互斥锁的相比,在获取锁失败的时候不会使得线程阻塞而是一直自旋尝试获取锁。当线程等待自旋锁的时候,CPU不能做其他事情,而是一直处于轮询忙等的状态。自旋锁主要适用于被持有时间短,线程不希望在重新调度上花过多时间的情况。实际上许多其他类型的锁在底层使用了自旋锁实现,例如多数互斥锁在试图获取锁的时候会先自旋一小段时间,然后才会休眠。如果在持锁时间很长的场景下使用自旋锁,则会导致CPU在这个线程的时间片用尽之前一直消耗在无意义的忙等上,造成计算资源的浪费。
使用自旋锁时要注意:
由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。
持有自旋锁的线程在sleep之前应该释放自旋锁以便其他咸亨可以获得该自旋锁
二、CAS操作实现自旋锁
CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,这种操作提供了硬件级别的原子操作(通过锁总线的方式)。CAS操作的原型可以认为是:
|
其中V代表内存中的变量,A代表期待的值,B表示新值。当V的值与A相等时,将V与B的值交换。逻辑上可以用下面的伪代码表示:
|
需要强调的是上面的操作是原子的,要么不做,要么全部完成。
那么已经拥有CAS操作的情况下如何实现一个自旋锁呢?首先回忆自旋锁的用途,本质上我们是希望能够让一个线程在不满足进入临界区的条件时,不停的忙等轮询,直到可以运行的时候再继续(进入临界区)执行。那么,我们可能自然的想到使用一个bool变量来表示是否可以进入临界区,例如以下面的伪代码的逻辑:
while
(flag ==
true
);
flag =
true
;
/*
do something ...
*/
flag =
false
;
...
这样做的直观想法是当flag为true的时候表示已经有线程处于临界区内,只有当flag为fasle时才能进入,而在进入的时候立即将flag置为true。但是这样做明显存在一个问题,判断flag为false和设置flag为true并不是一个不可分割的整体,有可能出现类似下面这样的时序, 假设最初flag为false:
step | thread 1 | thread 2 |
---|---|---|
1 | while(flag == true); | |
2 | while(flag == true); | |
3 | flag = true | |
4 | flag = true | |
5 | do something | do something |
6 | flag = false | |
7 | flag = false |
step是虚构的步骤,do something为一系列指令,这里写在一起表示并发执行。这里可以看出由于thread1读取判断flag的值与修改flag的值是两个独立的操作,中间插入了thread2的判断操作,最终使得有两个线程同时进入了临界区,这与我们的期望相悖。那么如何解决呢?如果能将读取判断与修改的操作合二为一,变成一个不可分割的整体,那么自然就不可能出现这种交错的场景。对于这样一个整体操作,我们希望它能读取内存中变量的值,并且当其等于特定值的时候,修改它为我们需要的另一个值。嗯......没错,这样我们就得到了CAS操作。
现在可以重新修改我们的同步方式,不停的进行期望flag为false的CAS操作 CAS(flag, flase, b) (这里b为true),直到其返回成功为止,再进行临界区中的操作,离开临界区时将flag置为false。
|
现在,判断操作与写入操作已经成为了一个整体,当一个线程的CAS操作成功的时候会阻止其他线程进入临界区,到达互斥访问的目的。
现在我们已经可以使用CAS操作来解决临界区的互斥访问的问题了,但是如果每次都这样写一遍实在太过麻烦,因此可以进行一些封装使得使用更加方便,也就是说...可以封装成自旋锁。我们可以用一个类来表示,将一个bool值作为类的数据成员,同时将CAS操作和赋值操作作为其成员函数,CAS操作其实就是加锁操作,而后面的赋值操作就是解锁操作。
三、用C++原子量实现
按照上面的思路,接下来用 C++ 11 引入标准库的原子量来实现一个自旋锁并且进行测试。
首先,我们需要一个bool值来表示锁的状态,这里直接使用标准库中的原子量 atomic<bool> (C++ 11的原子量可以参考:https://www.jb51.net/article/141896.htm ,在我的平台(Cygwin64、GCC7.3)上 atomic<bool> 的成员函数is_lock_free()返回值为true,是无锁的实现(如果内部使用了锁来实现的话那还叫什么自旋锁 = =)。实际上在大多数平台上 atomic<bool> 都是无锁的,如果不确定的话也可以使用C++标准规定必须为无锁实现的atomic_flag。
接下来,我们需要两个原子操作,CAS和赋值,C++11标准库在原子量的成员函数中直接提供了这两个操作。
|
compare_exchange_weak 与 compare_exchange_strong 主要的区别在于内存中的值与expected相等的时候,CAS操作是否一定能成功,compare_exchange_weak有概率会返回失败,而compare_exchange_strong则一定会成功。因此,compare_exchange_weak必须与循环搭配使用来保证在失败的时候重试CAS操作。得到的好处是在某些平台上compare_exchange_weak性能更好。按照上面的模型,我们本来就要和while搭配使用,可以使用compare_exchange_weak。最后内存序的选择没有特殊需求直接使用默认的std::memory_order_seq_cst。而赋值操作非常简单直接,这个调用一定会成功(只是赋值而已 = =),没有返回值。
实现代码非常短,下面是源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
如上面所说,lock操作不停的尝试CAS操作直到成功为止,unlock操作则将bool标志位复原。使用方式如下:
1 2 3 4 5 6 |
|
接下来,我们进行正确性测试,以经典的i++ 问题为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|
上面的代码中创建了10个线程对共享的全局变量count分别进行一百万次++操作,然后验证结果是否正确,最终执行的输出为:
1 2 3 4 5 6 |
|
从结果中可以看出我们实现的自旋锁起到了保护临界区(这里就是i++ )的作用,count最后的值等于每个线程执行自增的数目之和。作为对比,可以去掉IncCounter中的加锁解锁操作:
1 2 3 4 5 6 7 8 9 |
|
执行后的输出为:
1 2 3 4 5 6 |
|
结果由于多个线程同时执行 i++ 造成结果错误。
到这里,我们就通过 C++ 11的原子量实现了一个简单的自旋锁。这里只是对C++原子量的一个小使用,无论是自旋锁本身还是原子量都还有许多值得探究的地方。
利用C++11原子量atomic实现自旋锁详解相关推荐
- LINUX自旋锁详解2
对于互斥, 旗标是一个有用的工具, 但是它们不是内核提供的唯一这样的工具. 相反, 大部分加锁是由一种称为自旋锁的机制来实现. 不象旗标, 自旋锁可用在不能睡眠的代码中, 例如中断处理. 当正确地使用 ...
- LINUX自旋锁详解
加锁(locking)是一种广泛应用的同步技术.当内核控制路径必须访问共享数据结构或进入临界区时,就需要为自己获取一把"锁".由锁机制保护的资源非常类似于限制于房间内的资源,当某人 ...
- 读写自旋锁详解,第 3 部分(来自IBM)
基于简单共享变量的读写自旋锁的不足 本系列文章的第 2 部分中给出的实现都基于简单共享变量,简洁实用,但在大规模多核.NUMA 系统上可扩展性较差.我们说某个读写自旋锁的实现是可扩展的,通俗地讲是指在 ...
- 读写自旋锁详解,第 2 部分(来自IBM)
读者优先的读写自旋锁 我们先不考虑性能,搞出一个可用的实现再说.首先,用一个整型变量 status 来记录当前状态:另一个整型变量 nr_readers 来记录同时持有锁的读者数量,只有当 nr_re ...
- 读写自旋锁详解:TODO
林 昊翔 2011 年 7 月 21 日发布 Table of Contents 读写自旋锁简介 什么是读写自旋锁 读写自旋锁的属性 以自动机的观点看读写自旋锁 读写自旋锁的实现细节 读写自旋锁的接口 ...
- 【转】C++11 并发指南五(std::condition_variable 详解)
http://www.cnblogs.com/haippy/p/3252041.html 前面三讲<C++11 并发指南二(std::thread 详解)>,<C++11 并发指南三 ...
- C++11 并发指南五(std::condition_variable 详解)
前面三讲<C++11 并发指南二(std::thread 详解)>,<C++11 并发指南三(std::mutex 详解)>分别介绍了 std::thread,std::mut ...
- 多线程锁详解之【临界区】
更多的锁介绍可以先看看这篇文章:多线程锁详解之[序章] 正文: 一般锁的类型可分为两种:用户态锁和内核态锁.用户态锁是指这个锁的不能够跨进程使用.而内核态锁就是指能够跨进程使用的锁.一般书中会说,wi ...
- Redis分布式锁详解
Redis分布式锁详解 1. 分布式所概述 1.1 分布式锁 2. 缓存数据库Redis 2.1 redis简介 2.2 Springboot整合Redis两种方式 3. 实现验证 3.1 环境准备 ...
最新文章
- 输入一个链表,反转链表后,输出新链表的表头(ACM格式)(美团面试题)
- 【Android 逆向】函数拦截实例 ( ③ 刷新 CPU 高速缓存 | ④ 处理拦截函数 | ⑤ 返回特定结果 )
- Wireshark文档阅读笔记-TCP 4 times close解析与实例
- 什么是去中心化?交易所为什么要去中心化?
- 软件测试高频面试题真实分享/网上银行转账是怎么测的,设计一下测试用例。
- 解决问题#Word导出PDF出现空白页
- 解决连接远程服务器MySQL“ACCESS DENIED FOR USER‘ROOT‘@‘IP地址‘“问题
- GPGPU-Sim学习(二)搭建GPGPU-Sim环境(ubuntuServer 10.04 安装GPGPU-Sim)
- 利用Java语言编写一个猜数字游戏(有次数限制)
- ArcGIS API for JavaScript 4.2学习笔记[10] 2D添加指北针widget、视图保存、视图padding(第二章完结)...
- Activiti判断流程是否结束
- 数字信号处理之信号的抽取和内插
- gerrit git 邮箱不匹配的问题
- pyspark ValueError: Some of types cannot be determined after inferring
- 【纯JAVA语言做RPG游戏】1.做个瓷砖地图生成器
- %time与%timeit
- html图片占位符怎么设置,使用CSS3 Gradients创建图片占位符
- php写中国象棋,鲨鱼象棋V0.2.2我自己写的象棋软件欢迎测试
- CFA一级学习笔记--固定收益(三)--估值
- 编译mimikatz
热门文章
- 在手机端查看CAD图纸有什么技巧呢?
- Win7 任务栏缩略图消失的解决办法
- 【巨杉数据库SequoiaDB】巨杉⼯具系列之一 | ⼤对象存储⼯具sdblobtool
- java抄表电表,国内外常见抄表的电表有哪几种形式
- yii2的加密解密那些事儿
- Chocolat.js – 响应式的 jQuery Lightbox 插件
- Windows防止程序多开并在多开时弹出已运行程序
- 《阿凡达:水之道》顶级特效的背后
- 微信浏览器中iframe srcdoc、src=data:text/html,xxxx 等无法使用情况下防止样式污染的解决方案
- 手语服务让信息世界没有障碍