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


1. Go中的原子操作

原子性:一個(gè)或多個(gè)操作在CPU的執(zhí)行過程中不被中斷的特性,稱為原子性。這些操作對(duì)外表現(xiàn)成一個(gè)不可分割的整體,他們要么都執(zhí)行,要么都不執(zhí)行,外界不會(huì)看到他們只執(zhí)行到一半的狀態(tài)。

原子操作:進(jìn)行過程中不能被中斷的操作,原子操作由底層硬件支持,而鎖則是由操作系統(tǒng)提供的API實(shí)現(xiàn),若實(shí)現(xiàn)相同的功能,前者通常會(huì)更有效率

最小案例:

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不會(huì)等于1000,因?yàn)閏ount++這一步實(shí)際是三個(gè)操作:

從內(nèi)存讀取count

CPU更新count = count + 1

寫入count到內(nèi)存

因此就會(huì)出現(xiàn)多個(gè)goroutine讀取到相同的數(shù)值,然后更新同樣的數(shù)值到內(nèi)存,導(dǎo)致最終結(jié)果比預(yù)期少

2. Go中sync/atomic包

Go語(yǔ)言提供的原子操作都是非入侵式的,由標(biāo)準(zhǔn)庫(kù)中sync/aotomic中的眾多函數(shù)代表

atomic包中支持六種類型

int32

uint32

int64

uint64

uintptr

unsafe.Pointer

對(duì)于每一種類型,提供了五類原子操作:

LoadXXX(addr): 原子性的獲取*addr的值,等價(jià)于:

return *addr ? ? ?

StoreXXX(addr, val): 原子性的將val的值保存到*addr,等價(jià)于:

addr = val

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

*addr += delta

return *addr

SwapXXX(addr, new) old: 原子性的將new的值保存到*addr并返回舊值,等價(jià)于:

old = *addr

*addr = new

return old

CompareAndSwapXXX(addr, old, new) bool: 原子性的比較*addr和old,如果相同則將new賦值給*addr并返回true,等價(jià)于:

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. 擴(kuò)大原子操作的適用范圍:atomic.Value

Go語(yǔ)言在1.4版本的時(shí)候向sync/atomic包中添加了新的類型Value,此類型相當(dāng)于一個(gè)容器,被用來(lái)"原子地"存儲(chǔ)(Store)和加載任意類型的值

type Value func(v *Value) Load() (x interface{}): 讀操作,從線程安全的v中讀取上一步存放的內(nèi)容 func(v *Value) Store(x interface{}): 寫操作,將原始的變量x存放在atomic.Value類型的v中

比如作者寫文章時(shí)是22歲,寫著寫著就23歲了..

package main

import (

"fmt"

"sync"

"sync/atomic"

)

func main() {

// 此處依舊選用簡(jiǎn)單的數(shù)據(jù)類型,因?yàn)榇a量少

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()

// 在某一個(gè)goroutine中修改配置

if i == 0 {

config.Store(23)

}

// 輸出中夾雜22,23

fmt.Println(config.Load())

}(i)

}

wg.Wait()

}

4. atomic.Value源碼分析

atomic.Value被設(shè)計(jì)用來(lái)存儲(chǔ)任意類型的數(shù)據(jù),所以它內(nèi)部的字段是一個(gè)interface{}類型

type Value struct {

v interface{}

}

還有一個(gè)ifaceWords類型,作為空interface的內(nèi)部表示格式,typ代表原始類型,data代表真正的值

// ifaceWords is interface{} internal representation.

type ifaceWords struct {

typ? unsafe.Pointer

data unsafe.Pointer

}

4.1 unsafe.Pointer

Go語(yǔ)言并不支持直接操作內(nèi)存,但是它的標(biāo)準(zhǔn)庫(kù)提供一種不保證向后兼容的指針類型unsafe.Pointer, 讓程序可以靈活的操作內(nèi)存,它的特別之處在于:可以繞過Go語(yǔ)言類型系統(tǒng)的檢查

也就是說:如果兩種類型具有相同的內(nèi)存結(jié)構(gòu),我們可以將unsafe.Pointer當(dāng)作橋梁,讓這兩種類型的指針相互轉(zhuǎn)換,從而實(shí)現(xiàn)同一份內(nèi)存擁有兩種解讀方式

例如int類型和int32類型內(nèi)部的存儲(chǔ)結(jié)構(gòu)是一致的,但是對(duì)于指針類型的轉(zhuǎn)換需要這么做:

var a int32

// 獲得a的*int類型指針

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

4.2 實(shí)現(xiàn)原子性的讀取任意結(jié)構(gòu)操作

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

? ? // 將*Value指針類型轉(zhuǎn)換為*ifaceWords指針類型

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

// 原子性的獲取到v的類型typ的指針

typ := LoadPointer(&vp.typ)

// 如果沒有寫入或者正在寫入,先返回,^uintptr(0)代表過渡狀態(tài),見下文

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 實(shí)現(xiàn)原子性的存儲(chǔ)任意結(jié)構(gòu)操作

在此之前有一段較為重要的代碼,其中runtime_procPin方法可以將一個(gè)goroutine死死占用當(dāng)前使用的P (此處參考Goroutine調(diào)度器(一):P、M、G關(guān)系, 不發(fā)散了) 不允許其他的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")

}

// 將現(xiàn)有的值和要寫入的值轉(zhuǎn)換為ifaceWords類型,這樣下一步就能獲取到它們的原始類型和真正的值

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

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

for {

// 獲取現(xiàn)有的值的type

typ := LoadPointer(&vp.typ)

// 如果typ為nil說明這是第一次Store

if typ == nil {

// 如果你是第一次,就死死占住當(dāng)前的processor,不允許其他goroutine再搶

runtime_procPin()

// 使用CAS操作,先嘗試將typ設(shè)置為^uintptr(0)這個(gè)中間狀態(tài)

// 如果失敗,則證明已經(jīng)有別的線程搶先完成了賦值操作

// 那它就解除搶占鎖,然后重新回到 for 循環(huán)第一步

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

runtime_procUnpin()

continue

}

// 如果設(shè)置成功,說明當(dāng)前goroutine中了jackpot

// 那么就原子性的更新對(duì)應(yīng)的指針,最后解除搶占鎖

StorePointer(&vp.data, xp.data)

StorePointer(&vp.typ, xp.typ)

runtime_procUnpin()

return

}

// 如果typ為^uintptr(0)說明第一次寫入還沒有完成,繼續(xù)循環(huán)等待

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

continue

}

// 如果要寫入的類型和現(xiàn)有的類型不一致,則panic

if typ != xp.typ {

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

}

// 更新data

StorePointer(&vp.data, xp.data)

return

}

}

最后:


上面都是自己整理好的!我就把資料貢獻(xiàn)出來(lái)給有需要的人!順便求一波關(guān)注,哈哈~各位小伙伴關(guān)注我后私信【Java】就可以免費(fèi)領(lǐng)取噠

作者:Takagi_san

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文講解 golang 中 sync.atomic 的常見操作 atomic 提供的原子操作能夠確保任一時(shí)刻只有一...
    EasyHacking閱讀 2,587評(píng)論 0 0
  • sync/atomic包提供了原子操作的能力,直接有底層CPU硬件支持,因而一般要比基于操作系統(tǒng)API的鎖方式效率...
    坤_7a1e閱讀 2,641評(píng)論 0 0
  • 首先巴拉巴拉一下golang反射機(jī)制的三個(gè)定律 1.反射可以從接口類型到反射類型對(duì)象 2.反射可以從反射類型對(duì)象到...
    吃貓的魚0閱讀 3,091評(píng)論 0 1
  • go語(yǔ)言提供的原子操作都是非侵入式的,它們由標(biāo)準(zhǔn)庫(kù)代碼包sync/atomic中的眾多函數(shù)代表。 我們調(diào)用syn...
    吃貓的魚0閱讀 53,267評(píng)論 1 7
  • 大家早安[玫瑰]隨堂記錄療愈課羅老師課堂語(yǔ)錄分享給大家: 1.生命沒有危機(jī),危機(jī)就是轉(zhuǎn)機(jī)。 2.意愿要大于感覺,意...
    TIAN甜甜_7e97閱讀 431評(píng)論 0 0

友情鏈接更多精彩內(nèi)容