首页 网站首页 商业资讯 操作 查看内容

详解并发编程基础之原子操作(atomic包)

私域流量 操作 2023-4-4 13:21 7595人围观

前言

嗨,大师好,我是asong。比来想写一个并发编程系列的文章,利用Go也有一段时候了,可是对并发的了解不是很透彻,借着此次总结,希望能更进一步。我们以"原子操纵"开篇,对于并发操纵而言,原子操纵是个很是现实的题目,比力典型的利用的就是i++操纵,并发情况下,同时对内存中的i停止读取,就会发生与预期不符的成果,所以Go说话中的sync/atomic就是处理这个题目标,接下来我们一路来看一看Go的原子操纵。

什么是原子性、原子操纵

原子(atomic)本意是"不能被进一步朋分的最小粒子",而原子操纵(atomic operation)意为"不成中断的一个或一系列操纵"。实在用大口语说出来就是让多个线程对同一块内存的操纵是串行的,不会由于并发操纵把内存写的不合适预期。我们来看这样一个例子: 假定现在是一个银行账户系统,用户A想要自己从自己的账户直达1万元到用户B的账户上,直到转帐成功完成一个事务,首要做这两件事:

  • 从A的账户中减去1万元,假如A的账户本来就有2万元,现在就酿成了1万元
  • 给B的账户增加1万元,假如B的账户本来有2万元,那末现在就酿成了3万元

假定在操纵一的时辰,系统发生了故障,致使给B账户增加金钱失利了,那末就要停止回滚。回滚就是回到事务之前的状态,我们把这类要末一路成功的操纵叫做原子操纵,而原子性就是要末完整的被履行、要末完全不履行。

若何保证原子性

  • 锁机制

在处置器层面,可以采用总线加锁大概对缓存加锁的方式来实现多处置器之间的原子操纵。经过加锁保证从系统内存中读取或写入一个字节是原子的,也就是当一个处置器读取一个字节时,其他处置器不能拜候这个字节的内存地址。

总线锁:处置器供给一个Lock#信号,当一个处置器上在总线上输出此信号时,其他处置器的请求将被阻塞住,那末该处置器可以独占同享内存。总线锁会把CPU和内存之间的通讯锁住了,在锁定时代,其他处置就不能操纵其他内存地址的数据,所以总线锁定的开销比力大,所以处置会在某些场所利用缓存锁停止优化。 缓存锁:内存地区假如被缓存在处置器上的缓存行中,而且在Lock#操纵时代,那末当它履行操纵回写到内存时,处置不在总线上声言Lock#信号,而是点窜内部的内存地址,并答应它的缓存分歧机制来保证操纵的原子性,由于缓存分歧性机制会阻止同时点窜由两个以上处置器缓存的内存地区的数据,其他处置器回写已被锁定的缓存行的数据时,就会使缓存无效。

锁机制虽然可以保证原子性,可是锁机制会存在以下题目:

  • 多线程合作的情况下,频仍的加锁、开释锁会致使较多的高低文切换和调剂延时,性能会很差
  • 当一个线程占用时候比力长时,就多致使其他需要此锁的线程挂起.

上面我们说的都是灰心锁,要处理这类低效的题目,我们可以采用悲观锁,每次不加锁,而是假定没有抵触去完成某项操纵,假如由于抵触失利就重试,直到成功为止。也就是我们接下来要说的CAS(compare and swap).

  • CAS(compare and swap)

CAS的全称为Compare And Swap,直译就是比力交换。是一条CPU的原子指令,其感化是让CPU先辈行比力两个值能否相称,然后原子地更新某个位置的值,实在现方式是赐与硬件平台的汇编指令,在intelCPU中,利用的cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效力。简述进程是这样:

假定包括3个参数内存位置(V)、预期原值(A)和新值(B)。V暗示要更新变量的值,E暗示预期值,N暗示新值。仅当V值即是E值时,才会将V的值设为N,假如V值和E值分歧,则说明已经有其他线程在做更新,则当火线程什么都不做,最初CAS返回当前V的实在值。CAS操纵时抱着悲观的态度停止的,它总是以为自己可以成功完成操纵。基于这样的道理,CAS操纵即使没有锁,也可以发现其他线程对于当火线程的干扰。

伪代码可以这样写:

func CompareAndSwap(int *addr,int oldValue,int newValue) bool{
if *addr == nil{
return false
}
if *addr == oldValue {
*addr = newValue
return true
}
return false
}

不外上面的代码能够会发生一个题目,也就是ABA题目,由于CAS需要在操纵值的时辰检查下值有没有发生变化,假如没有发生变化则更新,可是假如一个值本来是A,酿成了B,又酿成了A,那末利用CAS停止检查时会发现它的值没有发生变化,可是现实上却变化了。ABA题目标处理思绪就是利用版本号。在变量前面追加上版本号,每次变量更新的时辰把版本号加一,那末A-B-A 就会酿成1A-2B-3A。

go说话中若何停止原子操纵

Go说话标准库中,sync/atomic包将底层硬件供给的原子操纵封装成了Go的函数,首要分为5个系列的函数,别离是:

  • func SwapXXXX(addr *int32, new int32) (old int32)系列:实在就是原子性的将new值保存到*addr并返回旧值。代码暗示:
old = *addr
*addr = new
return old
  • func CompareAndSwapXXXX((addr *int64, old, new int64) (swAPPed bool)系列:其就是原子性的比力*addr和old的值,假如不异则将new赋值给*addr并返回真,代码暗示:
if *addr == old{
*addr = new
return ture
}
return false
  • func AddXXXX(addr *int64, delta int64) (new int64)系列:原子性的将val的值增加到*addr并返回新值。代码暗示:
*addr += delta
return *addr
  • func LoadXXXX(addr *uint32) (val uint32)系列:原子性的获得*addr的值
  • func StoreXXXX(addr *int32, val int32)原子性的将val值保存到*addr

Go说话在1.4版本时增加一个新的范例Value,此范例的值就相当于一个容器,可以被用来"原子地"存储(store)和加载(Load)肆意范例的值。这些利用起来都还比力简单,就不写例子了,接下来我们一路看一看这些方式是若何实现的。

源码剖析

由于系列比力多。底层实现的方式也大同小异常,这里就首要分析一下Value的实现方式吧。为什么不分析其他系列的呢?由于原子操纵由底层硬件支持,所以看其他系列实现都要看汇编,Go的汇编是基于Plan9的,这个汇编说话真的材料甚少,我也是真的不懂,水平不够,也不自讨苦吃了,等前面真的能看懂这些汇编了,再来分析吧。这个网站有一些关于plan9汇编的常识,有爱好可以看一看:http://doc.cat-v.org/plan_9/4th_edition/papers/asm。

Value结构

我们先来看一下Value的结构:

type Value struct {
v interface{}
}

Value结构里就只要一个字段,是interface范例,虽然这里是interface范例,可是这里要留意,第一次Store写入的范例就肯定了以后写入的范例,否则会发生panic。由于这里是interface范例,所以为了以后写入与读取操纵方便,又在这个包里界说了一个ifaceWords结构,实在他就是一个空interface,他的感化就是将interface 分化成范例和数值。结构以下:

// ifaceWords is interface{} internal representation.
type ifaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}

Value的写入操纵

我们一路来看一看他是若何实现写入操纵的:

// Store sets the value of the Value to x.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).
func (v *Value) Store(x interface{}) {
if x == nil {
panic("sync/atomic: store of nil value into Value")
}
vp := (*ifaceWords)(unsafe.Pointer(v))
xp := (*ifaceWords)(unsafe.Pointer(&x))
for {
typ := LoadPointer(&vp.typ)
if typ == nil {
// Attempt to start first store.
// Disable preemption so that other goroutines can use
// active spin wait to wait for completion; and so that
// GC does not see the fake type accidentally.
runtime_procPin()
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
runtime_procUnpin()
continue
}
// Complete first store.
StorePointer(&vp.data, xp.data)
StorePointer(&vp.typ, xp.typ)
runtime_procUnpin()
return
}
if uintptr(typ) == ^uintptr(0) {
// First store in progress. Wait.
// Since we disable preemption around the first store,
// we can wait with active spinning.
continue
}
// First store completed. Check type and overwrite data.
if typ != xp.typ {
panic("sync/atomic: store of inconsistently typed value into Value")
}
StorePointer(&vp.data, xp.data)
return
}
}

// Disable/enable preemption, implemented in runtime.
func runtime_procPin()
func runtime_procUnpin()

这段代码中的正文集已经告诉了我们,挪用Store方式写入的范例必须与愿范例不异,纷歧致便会发生panic。接下来分析代码实现:

  1. 首先判定条件写入参数不能为nil,否则触发panic
  2. 经过利用unsafe.PointeroldValuenewValue转换成ifaceWords范例。方便我们获得他的原始范例(typ)和值(data).
  3. 为了保证原子性,所以这里利用一个for换来处置,当已经有Store正在停止写入时,会停止期待.
  4. 假如还没写入过数据,那末获得不到原始范例,就会起头第一次写入操纵,这里会把先挪用runtime_procPin()方式制止调剂器对当前 goroutine 的抢占(preemption),这样也可以避免GC线程看到一假范例。
  5. 挪用CAS方式来判定当前地址能否有被抢占,这里大师能够对unsafe.Pointer(^uintptr(0))这一句话有点不大白,由因而第一个写入数据,之前是没稀有据的,所以经过这样一其中心值来做判定,假如失利就会消除抢占锁,消除制止调剂器,继续循环期待.
  6. 设备中心值成功后,我们接下来便可以平安的把v设为传入的新值了,这里会先写入值,在写入范例(typ),由于我们会按照ty来做完成判定。
  7. 第一次写入没完成,我们还会经过uintptr(typ) == ^uintptr(0)来停止判定,由于还是第一次放入的中心范例,他仍然会继续期待第一次完成。
  8. 假如第一次写入完成,会检查上一次写入的范例与此次写入的范例能否分歧,纷歧致则会抛出panic.

这里代码量没有几多,相信大师一定看懂了吧~。

Value的读操纵

先看一下代码:

// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (x interface{}) {
vp := (*ifaceWords)(unsafe.Pointer(v))
typ := LoadPointer(&vp.typ)
if typ == nil || uintptr(typ) == ^uintptr(0) {
// First store not yet completed.
return nil
}
data := LoadPointer(&vp.data)
xp := (*ifaceWords)(unsafe.Pointer(&x))
xp.typ = typ
xp.data = data
return
}

读取操纵的代码就很简单了: 1.第一步利用unsafe.PointeroldValue转换成ifaceWords范例,然后获得他的范例,假如没有范例大概范例进来中心值,那末说明现在还没数据大概第一次写入还没有完成。 2. 经过检查后,挪用LoadPointer方式可以获得他的值,然后机关一个新interfacetypdata返回。

小彩蛋

前面我们在说CAS时,说到了ABA题目,所以我就写了demo试一试Go标准库atomic.CompareAndSwapXXX方式能否有处理这个题目,看运转成果是没有,所以这里大师利用的时辰要留意一下(虽然我也没想到什么现在什么营业场景会出现这个题目,可是还是要留意一下,需要自己评价)。

func main() {
var share uint64 = 1
wg := sync.WaitGroup{}
wg.Add(3)
// 协程1,期望值是1,欲更新的值是2
go func() {
defer wg.Done()
swapped := atomic.CompareAndSwapUint64(&share,1,2)
fmt.Println("goroutine 1",swapped)
}()
// 协程2,期望值是1,欲更新的值是2
go func() {
defer wg.Done()
time.Sleep(5 * time.Millisecond)
swapped := atomic.CompareAndSwapUint64(&share,1,2)
fmt.Println("goroutine 2",swapped)
}()
// 协程3,期望值是2,欲更新的值是1
go func() {
defer wg.Done()
time.Sleep(1 * time.Millisecond)
swapped := atomic.CompareAndSwapUint64(&share,2,1)
fmt.Println("goroutine 3",swapped)
}()
wg.Wait()
fmt.Println("main exit")
}

总结

原子操纵是并发编程的一个根本,也是为我进修sync.once打根本,好啦,现在你们应当晓得下篇文章的内容是什么啦,敬请期待~。

好啦,这篇文章就到这里啦,本质三连(分享、点赞、在看)都是笔者延续创作更多优良内容的动力!

保举往期文章:

  • machinery-go异步使命行列
  • 详解defer实现机制
  • 真的了解interface了嘛
  • Leaf—Segment散布式ID天生系统(Golang实现版本)
  • 十张动图带你搞懂排序算法(附go实现代码)
  • go参数传递范例
  • 手把手教姐姐写消息行列
  • 常见口试题之缓存雪崩、缓存穿透、缓存击穿
  • 详解Context包,看这一篇就够了!!!
  • go-ElasticSearch入门看这一篇就够了(一)
  • 口试官:go中for-range利用过吗?这几个题目你能诠释一下缘由吗

高端人脉微信群

高端人脉微信群

人脉=钱脉,我们相信天下没有聚不拢的人脉,扫码进群找到你所需的人脉,对接你所需的资源。

商业合作微信

商业合作微信

本站创始人微信,13年互联网营销经验,擅长引流裂变、商业模式、私域流量,高端人脉资源丰富。

我有话说......
  • 海哥 2023-4-4 13:23
    楼主有点囫囵吞枣,还是搞清楚再写比较好。
  • 卷云层 2023-4-4 13:23
    我对value的写操作的第7步有疑问,这里什么情况下uintptr(typ)会等于^uintptr(0)?第一次写入没完成又是什么意思?在第6步的时候难道不是肯定可以写入完成了吗?
  • exkeen 2023-4-4 13:22
    [赞]

相关推荐

国产操作系统发布:手机、电脑应用都能兼容

国产操作系统发布:手机、电脑应用都能兼容

近日深度操作系统官方宣布,国产操作系统deepin 20.6版本正式上线,新版本升级了Stabl

键盘操作方法大全

键盘操作方法大全

【键盘操作方法大全】键盘可不仅仅能帮我们打字哦,还有很多快捷的操作你都知道吗?除

电脑的一些基本常识和操作

电脑的一些基本常识和操作

关于电脑的一些基本常识和操作(电脑初学者必备)  众所周知,在21世纪的今天,电脑

操作系统实验一到实验九合集(哈工大李治军)

操作系统实验一到实验九合集(哈工大李治军)

知乎Markdown适配不行,希望在我的博客中查看文章作者寄语操作系统实验的学习是一个循

看完这篇Linux基本的操作就会了

看完这篇Linux基本的操作就会了

前言只有光头才能变强这个学期开了Linux的课程了,授课的老师也是比较负责任的一位。

搞懂软考,看这一篇就够了

搞懂软考,看这一篇就够了

大家好,我是你们的新朋友叨叨张,很高兴能够在这里和大家相遇,今天我要分享的主题是

0基础入门Photoshop基础操作(一)

0基础入门Photoshop基础操作(一)

大家好我是正经人你以为上来就要教封面上那个效果吗?当然不是,那个是我好几年前做的

从操作系统的进化中,读懂MagicOS

从操作系统的进化中,读懂MagicOS

操作系统的数十年沉浮1946年诞生第一台计算机时,还没有操作系统。程序员靠着「打孔」

还不会使用 GitHub ? GitHub 教程来了!万字图文详解

还不会使用 GitHub ? GitHub 教程来了!万字图文详解

在编程届有个共识,想要成为一个合格的程序员必须要掌握 GitHub 的用法!接下来,我们

Git使用教程,最详细,最傻瓜,最浅显,真正手把手教

Git使用教程,最详细,最傻瓜,最浅显,真正手把手教

(预警:因为详细,所以行文有些长,新手边看边操作效果出乎你的预料)一:Git是什么

Win10系统常用的快捷键(绝对很详细)

Win10系统常用的快捷键(绝对很详细)

前言介绍快捷键,也就是刷刷按几下键盘上的组合键就可以达到鼠标点很多下才能实现的效

上海医保转入杭州- “浙里办”简易操作

上海医保转入杭州- “浙里办”简易操作

我在上篇文章说过,上海医保需要社保(即养老保险)成功转入杭州后才能进行转移,申请

史上最全高中化学实验总结(操作+方法)

史上最全高中化学实验总结(操作+方法)

高中化学实验真复杂,包学习APP为你整理最全总结,不怕记不住!一、中学化学实验操作

大学四年自学走来,关于怎么学「操作系统」和「计算机网络 ... ...

大学四年自学走来,关于怎么学「操作系统」和「计算机网络 ... .

最近收到不少读者留言,关于怎么学「操作系统」和「计算机网络」的留言,小林写这一块

用这6款软件记笔记,不要太爽!丨上进青年研习社

用这6款软件记笔记,不要太爽!丨上进青年研习社

文/小渔俗话说:“好记性不如烂笔头。”在无纸时代,记笔记当然也不一定要用烂笔头了

8个流氓软件,这辈子是不可能安装的。

8个流氓软件,这辈子是不可能安装的。

之前安利过不少值得安装或使用的软件,但这一次我想换个角度,写一些强烈不建议安装的

KMS服务,一句命令激活windows/office!

KMS服务,一句命令激活windows/office!

服务器地址:http://kms.03k.org(点击检查是否可用);服务作用:在线激活windows和off

Windows10 快速启动

Windows10 快速启动

从Windows8开始,Windows的开机速度有了极大的提高,这得益于一项新的功能:快速启动

最后教一次:完美解决电脑上的流氓软件

最后教一次:完美解决电脑上的流氓软件

国产流氓软件之所以流氓就流氓在 “ 买一赠N ”装一个软件,就会给你附赠N个流氓软件

推荐10个超好玩的网站,一打开就停不下来!

推荐10个超好玩的网站,一打开就停不下来!

推荐10个超好玩的网站,窥探别人的记忆,敲键盘听歌,办公偷懒神器,看中国古今妖怪…

电话咨询: 15924191378
添加微信