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

1. Go中的原子操做

原子性:一個或多個操做在CPU的執行過程當中不被中斷的特性,稱爲原子性。這些操做對外表現成一個不可分割的總體,他們要麼都執行,要麼都不執行,外界不會看到他們只執行到一半的狀態。git

原子操做:進行過程當中不能被中斷的操做,原子操做由底層硬件支持,而鎖則是由操做系統提供的API實現,若實現相同的功能,前者一般會更有效率github

最小案例:golang

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++這一步實際是三個操做:api

  • 從內存讀取count
  • CPU更新count = count + 1
  • 寫入count到內存

所以就會出現多個goroutine讀取到相同的數值,而後更新一樣的數值到內存,致使最終結果比預期少安全

2. Go中sync/atomic包

Go語言提供的原子操做都是非入侵式的,由標準庫中sync/aotomic中的衆多函數表明bash

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: 原子性的比較*addrold,若是相同則將new賦值給*addr並返回true,等價於:
    if *addr == old {
        *addr = new
        return true
    }
    return false
    複製代碼

Go sync/atomic api

所以第一部分的案例能夠修改以下,便可經過post

// 修改方式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
	}
}
複製代碼

5. 參考

Go sync/atomic官方文檔

Go 中的原子操做 sync/atomic

Go 語言標準庫中 atomic.Value 的前世此生

Go 1.13中 sync.Pool 是如何優化的?

相關文章
相關標籤/搜索