Atomic Pointer in Go 1.19
本文也同样用于公司内部的技术分享。
随着 Go 1.19 的发布,Go 语言团队升级 sync/atomic, 在其中加入了 atomic.Pointer[T]
的新类型,也是第一个作为全新 API 加入标准库,支持泛型的数据类型,受到 Go 社区用户的关注。同时,官方在 Release Notes 中,也提及到这个 API 的加入,使得原子值使用更加简单。
atomic.Value 的时髦"替身" (sleeky alternative)
特征
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
仍然是对复合数据进行并发原子操作的好选择。