原子操作是比其它同步技術(shù)更基礎(chǔ)的操作。原子操作是無鎖的,常常直接通過CPU指令直接實(shí)現(xiàn)。 事實(shí)上,其它同步技術(shù)的實(shí)現(xiàn)常常依賴于原子操作。
注意,本文中的很多例子并非并發(fā)程序。它們只是用來演示如何使用sync/atomic
標(biāo)準(zhǔn)庫包中提供的原子操作。
對于一個(gè)整數(shù)類型T
,sync/atomic
標(biāo)準(zhǔn)庫包提供了下列原子操作函數(shù)。 其中T
可以是內(nèi)置int32
、int64
、uint32
、uint64
和uintptr
類型。
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引入了幾個(gè)各自擁有若干方法的類型用來實(shí)現(xiàn)上一節(jié)中列出的函數(shù)提供的同樣的功能。
在這些類型中,Int32
、Int64
、Uint32
、Uint64
和Uintptr
用來實(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ù)。
下面這個(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
}
StoreT
和LoadT
原子操作函數(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ù)類型,比如int32
或int64
,則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ì)分為兩類:
T
的一個(gè)變量值v
。
因?yàn)?code>-v在Go中是合法的,所以-v
可以直接被用做AddT
調(diào)用的第二個(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è)方法:Load
、Store
、Swap
和CompareAndSwap
(其中后兩個(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ù)具體需要選擇合適的方法。
為了便于理解和使用簡單,Go值的原子操作被設(shè)計(jì)的和內(nèi)存順序保證無關(guān)。 沒有任何官方文檔規(guī)定了原子操作應(yīng)該保證的內(nèi)存順序。 詳見Go中的內(nèi)存順序保證一文對此情況的說明。
更多建議: