Go语言的原子操作atomic
atomic
原子操作
原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。 事实上,其它同步技术的实现常常依赖于原子操作。
具体的原子操作在不同的操作系统中实现是不同的。比如在Intel的CPU架构机器上,主要是使用总线锁的方式实现的。 大致的意思就是当一个CPU需要操作一个内存块的时候,向总线发送一个LOCK信号,所有CPU收到这个信号后就不对这个内存块进行操作了。 等待操作的CPU执行完操作后,发送UNLOCK信号,才结束。 在AMD的CPU架构机器上就是使用MESI一致性协议的方式来保证原子操作。 所以我们在看atomic源码的时候,我们看到它针对不同的操作系统有不同汇编语言
文件。
Go中原子操作的支持
Go语言的sync/atomic
提供了对原子操作的支持,用于同步访问整数和指针。
- Go语言提供的原子操作都是非入侵式的
- 原子操作支持的类型包括
int32、int64、uint32、uint64、uintptr、unsafe.Pointer
。
CAS
go中的Cas操作,是借用了CPU提供的原子性指令来实现。CAS操作修改共享变量时候不需要对共享变量加锁,而是通过类似乐观锁的方式进行检查,本质还是不断的占用CPU 资源换取加锁带来的开销(比如上下文切换开销)。
原子操作中的CAS(Compare And Swap),在sync/atomic
包中,这类原子操作由名称以CompareAndSwap为前缀的若干个函数提供
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
调用函数后,CompareAndSwap
函数会先判断参数addr指向的操作值与参数old的值是否相等,仅当此判断得到的结果是true之后,才会用参数new代表的新值替换掉原先的旧值,否则操作就会被忽略。
我们使用的mutex
互斥锁类似悲观锁,总是假设会有并发的操作要修改被操作的值,所以使用锁将相关操作放入到临界区加以保存。而CAS操作做法趋于乐观锁,总是假设被操作的值未曾改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。在被操作值被频繁变更的情况下,CAS操作并不那么容易成功所以需要不断进行尝试,直到成功为止。
举个栗子
func main() {
fmt.Println("======old value=======")
fmt.Println(value)
addValue(10)
fmt.Println("======New value=======")
fmt.Println(value)
}
//不断地尝试原子地更新value的值,直到操作成功为止
func addValue(delta int32) {
for {
v := value
if atomic.CompareAndSwapInt32(&value, v, v+delta) {
break
}
}
}
这一系列的函数需要比较后再进行交换,也有不需要进行比较就进行交换的原子操作。
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
竞争条件是由于异步的访问共享资源,并试图同时读写该资源而导致的,使用互斥锁和通道的思路都是在线程获得到访问权后阻塞其他线程对共享内存的访问,而使用原子操作解决数据竞争问题则是利用了其不可被打断的特性。
增加或减少
对一个数值进行增加或者减少的行为也需要保证是原子的,它对应于atomic包的函数就是
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
读取或写入
当我们要读取一个变量的时候,很有可能这个变量正在被写入,这个时候,我们就很有可能读取到写到一半的数据。 所以读取操作是需要一个原子行为的。
在atomic包中就是Load开头的函数群。
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
读取是有原子性的操作的,同样写入atomic包也提供了相关的操作包。
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
原子操作与互斥锁的区别
首先atomic操作的优势是更轻量,比如CAS可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。
原子操作也有劣势。还是以CAS操作为例,使用CAS操作的做法趋于乐观,总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换,那么在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。而使用互斥锁的做法则趋于悲观,我们总假设会有并发的操作要修改被操作的值,并使用锁将相关操作放入临界区中加以保护。
下面是几点区别:
- 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作
- 原子操作是无锁的,常常直接通过CPU指令直接实现
- 原子操作中的cas趋于乐观锁,CAS操作并不那么容易成功,需要判断,然后尝试处理
- 可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
atomic包提供了底层的原子性内存原语,这对于同步算法的实现很有用。这些函数一定要非常小心地使用,使用不当反而会增加系统资源的开销,对于应用层来说,最好使用通道或sync包中提供的功能来完成同步操作。
针对atomic包的观点在Google的邮件组里也有很多讨论,其中一个结论解释是:
应避免使用该包装。或者,阅读C ++ 11标准的“原子操作”一章;如果您了解如何在C ++中安全地使用这些操作,那么你才能有安全地使用Go的sync/atomic包的能力。
参考
【Go并发编程之美-CAS操作】https://zhuanlan.zhihu.com/p/56733484
【sync/atomic - 原子操作】https://docs.kilvn.com/The-Golang-Standard-Library-by-Example/chapter16/16.02.html
【Go语言的原子操作和互斥锁的区别】https://studygolang.com/articles/29240
【Package atomic】https://go-zh.org/pkg/sync/atomic/
原文地址:https://www.cnblogs.com/ricklz/p/13648859.html
- 1637: [Usaco2007 Mar]Balanced Lineup
- AutoFac在项目中的应用
- 每天学一点Docker(5)——了解Docker架构
- 跨站请求伪造(CSRF/XSRF)
- 我这么玩Web Api(一)
- 1624: [Usaco2008 Open] Clear And Present Danger 寻宝之路
- 点双连通分量与割点
- 1648: [Usaco2006 Dec]Cow Picnic 奶牛野餐
- 1641: [Usaco2007 Nov]Cow Hurdles 奶牛跨栏
- 1668: [Usaco2006 Oct]Cow Pie Treasures 馅饼里的财富
- 2463: [中山市选2009]谁能赢呢?
- 2748: [HAOI2012]音量调节
- 2697: 特技飞行
- 我这么玩Web Api(二)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Centreon+Nagios实战第七篇——安装NRPE
- Python自学成才之路 详解类的三个重要方法__new__,__init__,__call__
- Centreon+Nagios实战第五篇——监控端安装Centreon
- Centreon+Nagios实战第四篇——监控端安装NDOUtils
- python自学成才之路 类属性和实例属性,__slots__方法
- 算法初步 基本概念 最大子数组和
- Oracle数据库名、实例名、ORACLE_SID、数据库域名、全局数据库名、服务名详解
- Maven实战之旅第六篇——maven常用指令
- maven实战之旅第四篇——利用maven archetype手动建立一个maven项目
- 一起刷 leetcode 之旋转矩阵
- exe调用DLL的方式
- 聊聊claudb的DatabaseCleaner
- Stata | 排名转为得分
- kafka使用avro序列化和反序列化
- avro使用schema生成java文件