Go - atomic包使用及atomic.Value源码分析


1. Go中的原子操作

原子性:一个或多个操作在CPU的执行过程中不被中断的特性,称为原子性。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。

原子操作:进行过程中不能被中断的操作,原子操作由底层硬件支持,而锁则是由操作系统提供的API实现,若实现相同的功能,前者通常会更有效率

最小案例:

package main

import (

"sync"

"fmt"

)

var count int

func add(wg *sync.WaitGroup) {

defer wg.Done()

count++

}

func main() {

wg := sync.WaitGroup{}

wg.Add(1000)

for i := 0; i < 1000; i++ {

go add(&wg)

}

wg.Wait()

fmt.Println(count)

}

count不会等于1000,因为count++这一步实际是三个操作:

从内存读取count

CPU更新count = count + 1

写入count到内存

因此就会出现多个goroutine读取到相同的数值,然后更新同样的数值到内存,导致最终结果比预期少

2. Go中sync/atomic包

Go语言提供的原子操作都是非入侵式的,由标准库中sync/aotomic中的众多函数代表

atomic包中支持六种类型

int32

uint32

int64

uint64

uintptr

unsafe.Pointer

对于每一种类型,提供了五类原子操作:

LoadXXX(addr): 原子性的获取*addr的值,等价于:

return *addr      

StoreXXX(addr, val): 原子性的将val的值保存到*addr,等价于:

addr = val

AddXXX(addr, delta): 原子性的将delta的值添加到*addr并返回新值(unsafe.Pointer不支持),等价于:

*addr += delta

return *addr

SwapXXX(addr, new) old: 原子性的将new的值保存到*addr并返回旧值,等价于:

old = *addr

*addr = new

return old

CompareAndSwapXXX(addr, old, new) bool: 原子性的比较*addr和old,如果相同则将new赋值给*addr并返回true,等价于:

if *addr == old {

    *addr = new

    return true

}

return false

因此第一部分的案例可以修改如下,即可通过

// 修改方式1

func add(wg *sync.WaitGroup) {

defer wg.Done()

for {

if atomic.CompareAndSwapInt32(&count, count, count+1) {

break

}

}

}

// 修改方式2

func add(wg *sync.WaitGroup) {

defer wg.Done()

atomic.AddInt32(&count, 1)

}

3. 扩大原子操作的适用范围:atomic.Value

Go语言在1.4版本的时候向sync/atomic包中添加了新的类型Value,此类型相当于一个容器,被用来"原子地"存储(Store)和加载任意类型的值

type Value func(v *Value) Load() (x interface{}): 读操作,从线程安全的v中读取上一步存放的内容 func(v *Value) Store(x interface{}): 写操作,将原始的变量x存放在atomic.Value类型的v中

比如作者写文章时是22岁,写着写着就23岁了..

package main

import (

"fmt"

"sync"

"sync/atomic"

)

func main() {

// 此处依旧选用简单的数据类型,因为代码量少

config := atomic.Value{}

config.Store(22)

wg := sync.WaitGroup{}

wg.Add(10)

for i := 0; i < 10; i++ {

go func(i int) {

defer wg.Done()

// 在某一个goroutine中修改配置

if i == 0 {

config.Store(23)

}

// 输出中夹杂22,23

fmt.Println(config.Load())

}(i)

}

wg.Wait()

}

4. atomic.Value源码分析

atomic.Value被设计用来存储任意类型的数据,所以它内部的字段是一个interface{}类型

type Value struct {

v interface{}

}

还有一个ifaceWords类型,作为空interface的内部表示格式,typ代表原始类型,data代表真正的值

// ifaceWords is interface{} internal representation.

type ifaceWords struct {

typ  unsafe.Pointer

data unsafe.Pointer

}

4.1 unsafe.Pointer

Go语言并不支持直接操作内存,但是它的标准库提供一种不保证向后兼容的指针类型unsafe.Pointer, 让程序可以灵活的操作内存,它的特别之处在于:可以绕过Go语言类型系统的检查

也就是说:如果两种类型具有相同的内存结构,我们可以将unsafe.Pointer当作桥梁,让这两种类型的指针相互转换,从而实现同一份内存拥有两种解读方式

例如int类型和int32类型内部的存储结构是一致的,但是对于指针类型的转换需要这么做:

var a int32

// 获得a的*int类型指针

(*int)(unsafe.Pointer(&a))

4.2 实现原子性的读取任意结构操作

func (v *Value) Load() (x interface{}) {

    // 将*Value指针类型转换为*ifaceWords指针类型

vp := (*ifaceWords)(unsafe.Pointer(v))

// 原子性的获取到v的类型typ的指针

typ := LoadPointer(&vp.typ)

// 如果没有写入或者正在写入,先返回,^uintptr(0)代表过渡状态,见下文

if typ == nil || uintptr(typ) == ^uintptr(0) {

return nil

}

// 原子性的获取到v的真正的值data的指针,然后返回

data := LoadPointer(&vp.data)

xp := (*ifaceWords)(unsafe.Pointer(&x))

xp.typ = typ

xp.data = data

return

}

4.3 实现原子性的存储任意结构操作

在此之前有一段较为重要的代码,其中runtime_procPin方法可以将一个goroutine死死占用当前使用的P (此处参考Goroutine调度器(一):P、M、G关系, 不发散了) 不允许其他的goroutine抢占,而runtime_procUnpin则是释放方法

// Disable/enable preemption, implemented in runtime.

func runtime_procPin()

func runtime_procUnpin()

Store方法

func (v *Value) Store(x interface{}) {

if x == nil {

panic("sync/atomic: store of nil value into Value")

}

// 将现有的值和要写入的值转换为ifaceWords类型,这样下一步就能获取到它们的原始类型和真正的值

vp := (*ifaceWords)(unsafe.Pointer(v))

xp := (*ifaceWords)(unsafe.Pointer(&x))

for {

// 获取现有的值的type

typ := LoadPointer(&vp.typ)

// 如果typ为nil说明这是第一次Store

if typ == nil {

// 如果你是第一次,就死死占住当前的processor,不允许其他goroutine再抢

runtime_procPin()

// 使用CAS操作,先尝试将typ设置为^uintptr(0)这个中间状态

// 如果失败,则证明已经有别的线程抢先完成了赋值操作

// 那它就解除抢占锁,然后重新回到 for 循环第一步

if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {

runtime_procUnpin()

continue

}

// 如果设置成功,说明当前goroutine中了jackpot

// 那么就原子性的更新对应的指针,最后解除抢占锁

StorePointer(&vp.data, xp.data)

StorePointer(&vp.typ, xp.typ)

runtime_procUnpin()

return

}

// 如果typ为^uintptr(0)说明第一次写入还没有完成,继续循环等待

if uintptr(typ) == ^uintptr(0) {

continue

}

// 如果要写入的类型和现有的类型不一致,则panic

if typ != xp.typ {

panic("sync/atomic: store of inconsistently typed value into Value")

}

// 更新data

StorePointer(&vp.data, xp.data)

return

}

}

最后:


上面都是自己整理好的!我就把资料贡献出来给有需要的人!顺便求一波关注,哈哈~各位小伙伴关注我后私信【Java】就可以免费领取哒

作者:Takagi_san

链接:https://juejin.im/post/5e2c0bbb51882526b645602b

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容

  • 本文讲解 golang 中 sync.atomic 的常见操作 atomic 提供的原子操作能够确保任一时刻只有一...
    EasyHacking阅读 2,529评论 0 0
  • sync/atomic包提供了原子操作的能力,直接有底层CPU硬件支持,因而一般要比基于操作系统API的锁方式效率...
    坤_7a1e阅读 2,555评论 0 0
  • 首先巴拉巴拉一下golang反射机制的三个定律 1.反射可以从接口类型到反射类型对象 2.反射可以从反射类型对象到...
    吃猫的鱼0阅读 2,905评论 0 1
  • go语言提供的原子操作都是非侵入式的,它们由标准库代码包sync/atomic中的众多函数代表。 我们调用syn...
    吃猫的鱼0阅读 52,949评论 1 7
  • 大家早安[玫瑰]随堂记录疗愈课罗老师课堂语录分享给大家: 1.生命没有危机,危机就是转机。 2.意愿要大于感觉,意...
    TIAN甜甜_7e97阅读 371评论 0 0