atomic pointer in Go 1-19
隨着 Go 1.19 的發佈,Go 語言團隊升級 sync/atomic, 在其中加入了 atomic.Pointer[T]
的新類型,也是第一個作爲全新 API 加入標準庫,支持泛型的數據類型,受到 Go 社區用戶的關注。同時,官方在 Release Notes 中,也提及到這個 API 的加入,使得原子值使用更加簡單。
特徵
atomic.Pointer
是泛型類。與 atomic.Value
不同的是,從 Value 類中拿出的數據,需要進行斷言,才能取出需要的值。而 Pointer 得益於泛型,直接能得到對應的數據類型。
type ServerConn struct {
Connection net.Conn
ID string
Open bool
}
func main() {
aPointer := atomic.Pointer[ServerConn]{}
s := ServerConn{ID: "first_conn"}
aPointer.Store(&s)
fmt.Println(aPointer.Load())
aValue := atomic.Value{}
aValue.Store(&s)
conn, ok := aValue.Load().(*ServerConn)
if !ok {
panic("assert is not ok")
}
fmt.Println(conn)
}
輸出:
&{<nil> first_conn false}
&{<nil> first_conn false}
實現比較
Value
的 Store 操作,會在運行時檢查、確定其存儲的實際類型,如果傳入的是 nil 值的接口類型 (any) 數據,或者與上次 Store 的數據類型不同,則會 panic。
func (v *Value) Store(val any) {
if val == nil {
panic("sync/atomic: store of nil value into Value")
}
vp := (*ifaceWords)(unsafe.Pointer(v))
vlp := (*ifaceWords)(unsafe.Pointer(&val))
for {
typ := LoadPointer(&vp.typ)
if typ == nil {
// Attempt to start first store.
// Disable preemption so that other goroutines can use
// active spin wait to wait for completion.
runtime_procPin()
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
runtime_procUnpin()
continue
}
// Complete first store.
StorePointer(&vp.data, vlp.data)
StorePointer(&vp.typ, vlp.typ)
runtime_procUnpin()
return
}
if typ == unsafe.Pointer(&firstStoreInProgress) {
// First store in progress. Wait.
// Since we disable preemption around the first store,
// we can wait with active spinning.
continue
}
// First store completed. Check type and overwrite data.
if typ != vlp.typ {
panic("sync/atomic: store of inconsistently typed value into Value")
}
StorePointer(&vp.data, vlp.data)
return
}
}
// ifaceWords is interface{} internal representation.
type ifaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}
Pointer 的同樣操作,因會在編譯期確定類型,直接存儲指針即可。實現和調用簡單不少。
// Store atomically stores val into x.
func (x *Pointer[T]) Store(val *T) { StorePointer(&x.v, unsafe.Pointer(val)) }
數據競爭
以下代碼由於會在一定的時間內併發地讀寫,造成可能數據競爭。在 go run
命令中,加上 -race
參數,在代碼執行過程中,檢測可能會發生的數據競爭情況。
func ShowConnection(p *ServerConn) {
for {
time.Sleep(1 * time.Second)
fmt.Println(p, *p)
}
}
func main() {
c := make(chan bool)
p := ServerConn{ID: "first_conn"}
go ShowConnection(&p)
go func() {
for {
time.Sleep(3 * time.Second)
newConn := ServerConn{ID: "new_conn"}
p = newConn
}
}()
<-c
}
運行輸出
go run -race ./main.go
&{<nil> first_conn false} {<nil> first_conn false}
&{<nil> first_conn false} {<nil> first_conn false}
==================
WARNING: DATA RACE
Write at 0x00c00013e870 by goroutine 8:
main.main.func1()
.../main.go:53 +0x6a
Previous read at 0x00c00013e870 by goroutine 7:
runtime.convT()
../go/src/runtime/iface.go:321 +0x0
main.ShowConnection()
.../main.go:41 +0x64
main.main.func2()
.../main.go:48 +0x39
Goroutine 8 (running) created at:
main.main()
.../main.go:49 +0x16e
Goroutine 7 (running) created at:
main.main()
.../main.go:48 +0x104
==================
&{<nil> new_conn false} {<nil> new_conn false}
&{<nil> new_conn false} {<nil> new_conn false}
使用 atomic.Pointer
將上文的指針,換成 atomic.Pointer
後,執行時,沒有數據競爭的警告。
func ShowConnection(p *atomic.Pointer[ServerConn]) {
for {
time.Sleep(1 * time.Second)
fmt.Println(p.Load())
}
}
func main() {
c := make(chan bool)
p := atomic.Pointer[ServerConn]{}
s := ServerConn{ID: "first_conn"}
p.Store(&s)
go ShowConnection(&p)
go func() {
for {
time.Sleep(3 * time.Second)
newConn := ServerConn{ID: "new_conn"}
p.Swap(&newConn)
}
}()
<-c
}
輸出
go run -race .\main.go
&{<nil> first_conn false}
&{<nil> first_conn false}
&{<nil> new_conn false}
&{<nil> new_conn false}
&{<nil> new_conn false}
原子操作是互斥鎖以外,另一種操作共享資源的方法。atomic.Pointer
的加入,使得對指針類型數據的操作友好易用。 在不方便使用新特性的情況下,atomic.Value
仍然是對複合數據進行併發原子操作的好選擇。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mizuame.moe/posts/atomic_pointer/