Go語言 原子操作 - 如何使用sync/atomic標(biāo)準(zhǔn)庫包

2023-02-16 17:39 更新

原子操作是比其它同步技術(shù)更基礎(chǔ)的操作。原子操作是無鎖的,常常直接通過CPU指令直接實(shí)現(xiàn)。 事實(shí)上,其它同步技術(shù)的實(shí)現(xiàn)常常依賴于原子操作。

注意,本文中的很多例子并非并發(fā)程序。它們只是用來演示如何使用sync/atomic標(biāo)準(zhǔn)庫包中提供的原子操作。

Go 1.19之前的版本中支持的原子操作概述

對于一個(gè)整數(shù)類型T,sync/atomic標(biāo)準(zhǔn)庫包提供了下列原子操作函數(shù)。 其中T可以是內(nèi)置int32int64、uint32、uint64uintptr類型。

func AddT(addr *T, delta T)(new T)
func LoadT(addr *T) (val T)
func StoreT(addr *T, val T)
func SwapT(addr *T, new T) (old T)
func CompareAndSwapT(addr *T, old, new T) (swapped bool)

比如,下列五個(gè)原子操作函數(shù)提供給了內(nèi)置int32類型。

func AddInt32(addr *int32, delta int32)(new int32)
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
func SwapInt32(addr *int32, new int32) (old int32)
func CompareAndSwapInt32(addr *int32,
				old, new int32) (swapped bool)

下列四個(gè)原子操作函數(shù)提供給了(安全)指針類型。 因?yàn)檫@幾個(gè)函數(shù)被引入標(biāo)準(zhǔn)庫的時(shí)候,Go還不支持自定義泛型,所以這些函數(shù)是通過非類型安全指針unsafe.Pointer來實(shí)現(xiàn)的。

func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer,
				) (old unsafe.Pointer)
func CompareAndSwapPointer(addr *unsafe.Pointer,
				old, new unsafe.Pointer) (swapped bool)

因?yàn)镚o(安全)指針不支持算術(shù)運(yùn)算,所以相對于整數(shù)類型,指針類型的原子操作少了一個(gè)AddPointer函數(shù)。

sync/atomic標(biāo)準(zhǔn)庫包也提供了一個(gè)Value類型,以它為基的指針類型*Value擁有四個(gè)方法(見下,其中后兩個(gè)是從Go 1.17開始才支持的)。 Value值用來原子讀取和修改任何類型的Go值。

func (*Value) Load() (x interface{})
func (*Value) Store(x interface{})
func (*Value) Swap(new interface{}) (old interface{})
func (*Value) CompareAndSwap(old, new interface{}) (swapped bool)

Go 1.19+ 版本中新增的原子操作概述

Go 1.19引入了幾個(gè)各自擁有若干方法的類型用來實(shí)現(xiàn)上一節(jié)中列出的函數(shù)提供的同樣的功能。

在這些類型中,Int32、Int64Uint32、Uint64Uintptr用來實(shí)現(xiàn)整數(shù)原子操作。 下面列出的是atomic.Int32類型的方法。其它四個(gè)類型的方法是類似的。

func (*Int32) Add(delta int32) (new int32)
func (*Int32) Load() int32
func (*Int32) Store(val int32)
func (*Int32) Swap(new int32) (old int32)
func (*Int32) CompareAndSwap(old, new int32) (swapped bool)

從Go 1.18開始,Go已經(jīng)開始支持自定義泛型。 一些標(biāo)準(zhǔn)庫包開始在Go 1.19中使用自定義泛型,這其中包括sync/atomic標(biāo)準(zhǔn)庫包。 此包在Go 1.19中引入的atomic.Pointer[T any]類型就是一個(gè)泛型類型。 下面列出了它的方法:

(*Pointer[T]) Load() *T
(*Pointer[T]) Store(val *T)
(*Pointer[T]) Swap(new *T) (old *T)
(*Pointer[T]) CompareAndSwap(old, new *T) (swapped bool)

Go 1.19也引入了一個(gè)Bool類型來進(jìn)行布爾原子操作。

整數(shù)原子操作

本文的余下部分將通過一些示例來展示如何使用這些原子操作函數(shù)。

下面這個(gè)例子展示了如何使用Add原子操作來并發(fā)地遞增一個(gè)int32值。 在此例子中,主協(xié)程中創(chuàng)建了1000個(gè)新協(xié)程。每個(gè)新協(xié)程將整數(shù)n的值增加1。 原子操作保證這1000個(gè)新協(xié)程之間不會(huì)發(fā)生數(shù)據(jù)競爭。此程序肯定打印出1000

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var n int32
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			atomic.AddInt32(&n, 1)
			wg.Done()
		}()
	}
	wg.Wait()

	fmt.Println(atomic.LoadInt32(&n)) // 1000
}

如果我們將新協(xié)程中的語句atomic.AddInt32(&n, 1)替換為n++,則最后的輸出結(jié)果很可能不是1000。

下面的代碼使用Go 1.19引入的atomic.Int32類型和它的方法重新實(shí)現(xiàn)了上面的程序。 此實(shí)現(xiàn)略顯整潔。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var n atomic.Int32
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			n.Add(1)
			wg.Done()
		}()
	}
	wg.Wait()

	fmt.Println(n.Load()) // 1000
}

StoreTLoadT原子操作函數(shù)或者方法經(jīng)常被用來需要并發(fā)運(yùn)行的實(shí)現(xiàn)setter和getter方法。 下面的例子使用了原子操作函數(shù):

type Page struct {
	views uint32
}

func (page *Page) SetViews(n uint32) {
	atomic.StoreUint32(&page.views, n)
}

func (page *Page) Views() uint32 {
	return atomic.LoadUint32(&page.views)
}

下面這個(gè)例子使用了Go 1.19引入的類型和方法:

type Page struct {
	views atomic.Uint32
}

func (page *Page) SetViews(n uint32) {
	page.views.Store(n)
}

func (page *Page) Views() uint32 {
	return page.views.Load()
}

如果T是一個(gè)有符號整數(shù)類型,比如int32int64,則AddT函數(shù)調(diào)用的第二個(gè)實(shí)參可以是一個(gè)負(fù)數(shù),用來實(shí)現(xiàn)原子減法操作。 但是如果T是一個(gè)無符號整數(shù)類型,比如uint32、uint64或者uintptr,則AddT函數(shù)調(diào)用的第二個(gè)實(shí)參需要為一個(gè)非負(fù)數(shù),那么如何實(shí)現(xiàn)無符號整數(shù)類型T值的原子減法操作呢? 畢竟sync/atomic標(biāo)準(zhǔn)庫包沒有提供SubstractT函數(shù)。 根據(jù)欲傳遞的第二個(gè)實(shí)參的特點(diǎn),我們可以把T為一個(gè)無符號整數(shù)類型的情況細(xì)分為兩類:

  1. 第二個(gè)實(shí)參為類型為T的一個(gè)變量值v。 因?yàn)?code>-v在Go中是合法的,所以-v可以直接被用做AddT調(diào)用的第二個(gè)實(shí)參。
  2. 第二個(gè)實(shí)參為一個(gè)正整數(shù)常量c,這時(shí)-c在Go中是編譯不通過的,所以它不能被用做AddT調(diào)用的第二個(gè)實(shí)參。 這時(shí)我們可以使用^T(c-1)(仍為一個(gè)正數(shù))做為AddT調(diào)用的第二個(gè)實(shí)參。

^T(v-1)小技巧對于無符號類型的變量v也是適用的,但是^T(v-1)T(-v)的效率要低。

對于這個(gè)^T(c-1)小技巧,如果c是一個(gè)類型確定值并且它的類型確實(shí)就是T,則它的表示形式可以簡化為^(c-1)。

一個(gè)例子:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var (
		n uint64 = 97
		m uint64 = 1
		k int    = 2
	)
	const (
		a        = 3
		b uint64 = 4
		c uint32 = 5
		d int    = 6
	)

	show := fmt.Println
	atomic.AddUint64(&n, -m)
	show(n) // 96 (97 - 1)
	atomic.AddUint64(&n, -uint64(k))
	show(n) // 94 (96 - 2)
	atomic.AddUint64(&n, ^uint64(a - 1))
	show(n) // 91 (94 - 3)
	atomic.AddUint64(&n, ^(b - 1))
	show(n) // 87 (91 - 4)
	atomic.AddUint64(&n, ^uint64(c - 1))
	show(n) // 82 (87 - 5)
	atomic.AddUint64(&n, ^uint64(d - 1))
	show(n) // 76 (82 - 6)
	x := b; atomic.AddUint64(&n, -x)
	show(n) // 72 (76 - 4)
	atomic.AddUint64(&n, ^(m - 1))
	show(n) // 71 (72 - 1)
	atomic.AddUint64(&n, ^uint64(k - 1))
	show(n) // 69 (71 - 2)
}

SwapT函數(shù)調(diào)用和StoreT函數(shù)調(diào)用類似,但是返回修改之前的舊值(因此稱為置換操作)。

一個(gè)CompareAndSwapT函數(shù)調(diào)用傳遞的舊值和目標(biāo)值的當(dāng)前值匹配的情況下才會(huì)將目標(biāo)值改為新值,并返回true;否則立即返回false。

一個(gè)例子:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var n int64 = 123
	var old = atomic.SwapInt64(&n, 789)
	fmt.Println(n, old) // 789 123
	swapped := atomic.CompareAndSwapInt64(&n, 123, 456)
	fmt.Println(swapped) // false
	fmt.Println(n)       // 789
	swapped = atomic.CompareAndSwapInt64(&n, 789, 456)
	fmt.Println(swapped) // true
	fmt.Println(n)       // 456
}

下面是與之對應(yīng)的使用Go 1.19引入的類型和方法的實(shí)現(xiàn):

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var n atomic.Int64
	n.Store(123)
	var old = n.Swap(789)
	fmt.Println(n.Load(), old) // 789 123
	swapped := n.CompareAndSwap(123, 456)
	fmt.Println(swapped)  // false
	fmt.Println(n.Load()) // 789
	swapped = n.CompareAndSwap(789, 456)
	fmt.Println(swapped)  // true
	fmt.Println(n.Load()) // 456
}

請注意,到目前為止(Go 1.19),一個(gè)64位字(int64或uint64值)的原子操作要求此64位字的內(nèi)存地址必須是8字節(jié)對齊的。 對于Go 1.19引入的原子方法操作,此要求無論在32-bit還是64-bit架構(gòu)上總是會(huì)得到滿足,但是對于32-bit架構(gòu)上的原子函數(shù)操作,此要求并非總會(huì)得到滿足。 請閱讀關(guān)于Go值的內(nèi)存布局一文獲取詳情。

指針值的原子操作

上面已經(jīng)提到了sync/atomic標(biāo)準(zhǔn)庫包為指針值的原子操作提供了四個(gè)函數(shù),并且指針值的原子操作是通過非類型安全指針來實(shí)現(xiàn)的。

非類型安全指針一文,我們得知,在Go中, 任何指針類型的值可以被顯式轉(zhuǎn)換為非類型安全指針類型unsafe.Pointer,反之亦然。 所以指針類型*unsafe.Pointer的值也可以被顯式轉(zhuǎn)換為類型unsafe.Pointer,反之亦然。

下面這個(gè)程序不是一個(gè)并發(fā)程序。它僅僅展示了如何使用指針原子操作。在這個(gè)例子中,類型T可以為任何類型。

package main

import (
	"fmt"
	"sync/atomic"
	"unsafe"
)

type T struct {x int}

func main() {
	var pT *T
	var unsafePPT = (*unsafe.Pointer)(unsafe.Pointer(&pT))
	var ta, tb = T{1}, T{2}
	// 修改
	atomic.StorePointer(
		unsafePPT, unsafe.Pointer(&ta))
	fmt.Println(pT) // &{1}
	// 讀取
	pa1 := (*T)(atomic.LoadPointer(unsafePPT))
	fmt.Println(pa1 == &ta) // true
	// 置換
	pa2 := atomic.SwapPointer(
		unsafePPT, unsafe.Pointer(&tb))
	fmt.Println((*T)(pa2) == &ta) // true
	fmt.Println(pT) // &{2}
	// 比較置換
	b := atomic.CompareAndSwapPointer(
		unsafePPT, pa2, unsafe.Pointer(&tb))
	fmt.Println(b) // false
	b = atomic.CompareAndSwapPointer(
		unsafePPT, unsafe.Pointer(&tb), pa2)
	fmt.Println(b) // true
}

是的,目前指針的原子操作使用起來是相當(dāng)?shù)膯隆?事實(shí)上,啰嗦還是次要的,更主要的是,因?yàn)橹羔樀脑硬僮餍枰?code>unsafe標(biāo)準(zhǔn)庫包,所以這些操作函數(shù)不在Go 1兼容性保證之列。

與之相對,如果我們使用Go 1.19引入的Pointer泛型類型和它的方法來做指針原子操作,代碼將變得簡潔的多。 下面的代碼證明了這一點(diǎn)。

package main

import (
	"fmt"
	"sync/atomic"
)

type T struct {x int}

func main() {
	var pT atomic.Pointer[T]
	var ta, tb = T{1}, T{2}
	// store
	pT.Store(&ta)
	fmt.Println(pT.Load()) // &{1}
	// load
	pa1 := pT.Load()
	fmt.Println(pa1 == &ta) // true
	// swap
	pa2 := pT.Swap(&tb)
	fmt.Println(pa2 == &ta) // true
	fmt.Println(pT.Load())  // &{2}
	// compare and swap
	b := pT.CompareAndSwap(&ta, &tb)
	fmt.Println(b) // false
	b = pT.CompareAndSwap(&tb, &ta)
	fmt.Println(b) // true
}

更為重要的是,上面這段代碼沒有引入unsafe標(biāo)準(zhǔn)庫包,所以Go 1會(huì)保證它的向后兼容性。

任何類型值的原子操作

sync/atomic標(biāo)準(zhǔn)庫包中提供的Value類型可以用來讀取和修改任何類型的值。

類型*Value有幾個(gè)方法:LoadStore、SwapCompareAndSwap(其中后兩個(gè)方法實(shí)在Go 1.17中引入的)。 這些方法均以interface{}做為參數(shù)類型,所以傳遞給它們的實(shí)參可以是任何類型的值。 但是對于一個(gè)可尋址的Value類型的值v,一旦v.Store方法((&v).Store的簡寫形式)被曾經(jīng)調(diào)用一次,則傳遞給值v的后續(xù)方法調(diào)用的實(shí)參的具體類型必須和傳遞給它的第一次調(diào)用的實(shí)參的具體類型一致; 否則,將產(chǎn)生一個(gè)恐慌。nil接口類型實(shí)參也將導(dǎo)致v.Store()方法調(diào)用產(chǎn)生恐慌。

一個(gè)例子:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	type T struct {a, b, c int}
	var ta = T{1, 2, 3}
	var v atomic.Value
	v.Store(ta)
	var tb = v.Load().(T)
	fmt.Println(tb)       // {1 2 3}
	fmt.Println(ta == tb) // true

	v.Store("hello") // 將導(dǎo)致一個(gè)恐慌
}

另一個(gè)例子(針對Go 1.17+):

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	type T struct {a, b, c int}
	var x = T{1, 2, 3}
	var y = T{4, 5, 6}
	var z = T{7, 8, 9}
	var v atomic.Value
	v.Store(x)
	fmt.Println(v) // {{1 2 3}}
	old := v.Swap(y)
	fmt.Println(v)       // {{4 5 6}}
	fmt.Println(old.(T)) // {1 2 3}
	swapped := v.CompareAndSwap(x, z)
	fmt.Println(swapped, v) // false {{4 5 6}}
	swapped = v.CompareAndSwap(y, z)
	fmt.Println(swapped, v) // true {{7 8 9}}
}

事實(shí)上,我們也可以使用上一節(jié)介紹的指針原子操作來對任何類型的值進(jìn)行原子讀取和修改,不過需要多一級指針的間接引用。 兩種方法有各自的好處和缺點(diǎn)。在實(shí)踐中需要根據(jù)具體需要選擇合適的方法。

原子操作相關(guān)的內(nèi)存順序保證

為了便于理解和使用簡單,Go值的原子操作被設(shè)計(jì)的和內(nèi)存順序保證無關(guān)。 沒有任何官方文檔規(guī)定了原子操作應(yīng)該保證的內(nèi)存順序。 詳見Go中的內(nèi)存順序保證一文對此情況的說明。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號