Go语言 原子操作 - 如何使用sync/atomic标准库包

2023-02-16 17:39 更新

原子操作是比其它同步技术更基础的操作。原子操作是无锁的,常常直接通过CPU指令直接实现。 事实上,其它同步技术的实现常常依赖于原子操作。

注意,本文中的很多例子并非并发程序。它们只是用来演示如何使用sync/atomic标准库包中提供的原子操作。

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

对于一个整数类型Tsync/atomic标准库包提供了下列原子操作函数。 其中T可以是内置int32int64uint32uint64uintptr类型。

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)

比如,下列五个原子操作函数提供给了内置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)

下列四个原子操作函数提供给了(安全)指针类型。 因为这几个函数被引入标准库的时候,Go还不支持自定义泛型,所以这些函数是通过非类型安全指针unsafe.Pointer来实现的。

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)

因为Go(安全)指针不支持算术运算,所以相对于整数类型,指针类型的原子操作少了一个AddPointer函数。

sync/atomic标准库包也提供了一个Value类型,以它为基的指针类型*Value拥有四个方法(见下,其中后两个是从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引入了几个各自拥有若干方法的类型用来实现上一节中列出的函数提供的同样的功能。

在这些类型中,Int32Int64Uint32Uint64Uintptr用来实现整数原子操作。 下面列出的是atomic.Int32类型的方法。其它四个类型的方法是类似的。

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已经开始支持自定义泛型。 一些标准库包开始在Go 1.19中使用自定义泛型,这其中包括sync/atomic标准库包。 此包在Go 1.19中引入的atomic.Pointer[T any]类型就是一个泛型类型。 下面列出了它的方法:

(*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也引入了一个Bool类型来进行布尔原子操作。

整数原子操作

本文的余下部分将通过一些示例来展示如何使用这些原子操作函数。

下面这个例子展示了如何使用Add原子操作来并发地递增一个int32值。 在此例子中,主协程中创建了1000个新协程。每个新协程将整数n的值增加1。 原子操作保证这1000个新协程之间不会发生数据竞争。此程序肯定打印出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
}

如果我们将新协程中的语句atomic.AddInt32(&n, 1)替换为n++,则最后的输出结果很可能不是1000

下面的代码使用Go 1.19引入的atomic.Int32类型和它的方法重新实现了上面的程序。 此实现略显整洁。

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原子操作函数或者方法经常被用来需要并发运行的实现setter和getter方法。 下面的例子使用了原子操作函数:

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)
}

下面这个例子使用了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是一个有符号整数类型,比如int32int64,则AddT函数调用的第二个实参可以是一个负数,用来实现原子减法操作。 但是如果T是一个无符号整数类型,比如uint32uint64或者uintptr,则AddT函数调用的第二个实参需要为一个非负数,那么如何实现无符号整数类型T值的原子减法操作呢? 毕竟sync/atomic标准库包没有提供SubstractT函数。 根据欲传递的第二个实参的特点,我们可以把T为一个无符号整数类型的情况细分为两类:

  1. 第二个实参为类型为T的一个变量值v。 因为-v在Go中是合法的,所以-v可以直接被用做AddT调用的第二个实参。
  2. 第二个实参为一个正整数常量c,这时-c在Go中是编译不通过的,所以它不能被用做AddT调用的第二个实参。 这时我们可以使用^T(c-1)(仍为一个正数)做为AddT调用的第二个实参。

^T(v-1)小技巧对于无符号类型的变量v也是适用的,但是^T(v-1)T(-v)的效率要低。

对于这个^T(c-1)小技巧,如果c是一个类型确定值并且它的类型确实就是T,则它的表示形式可以简化为^(c-1)

一个例子:

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函数调用和StoreT函数调用类似,但是返回修改之前的旧值(因此称为置换操作)。

一个CompareAndSwapT函数调用传递的旧值和目标值的当前值匹配的情况下才会将目标值改为新值,并返回true;否则立即返回false

一个例子:

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
}

下面是与之对应的使用Go 1.19引入的类型和方法的实现:

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),一个64位字(int64或uint64值)的原子操作要求此64位字的内存地址必须是8字节对齐的。 对于Go 1.19引入的原子方法操作,此要求无论在32-bit还是64-bit架构上总是会得到满足,但是对于32-bit架构上的原子函数操作,此要求并非总会得到满足。 请阅读关于Go值的内存布局一文获取详情。

指针值的原子操作

上面已经提到了sync/atomic标准库包为指针值的原子操作提供了四个函数,并且指针值的原子操作是通过非类型安全指针来实现的。

非类型安全指针一文,我们得知,在Go中, 任何指针类型的值可以被显式转换为非类型安全指针类型unsafe.Pointer,反之亦然。 所以指针类型*unsafe.Pointer的值也可以被显式转换为类型unsafe.Pointer,反之亦然。

下面这个程序不是一个并发程序。它仅仅展示了如何使用指针原子操作。在这个例子中,类型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
}

是的,目前指针的原子操作使用起来是相当的啰嗦。 事实上,啰嗦还是次要的,更主要的是,因为指针的原子操作需要引入unsafe标准库包,所以这些操作函数不在Go 1兼容性保证之列。

与之相对,如果我们使用Go 1.19引入的Pointer泛型类型和它的方法来做指针原子操作,代码将变得简洁的多。 下面的代码证明了这一点。

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标准库包,所以Go 1会保证它的向后兼容性。

任何类型值的原子操作

sync/atomic标准库包中提供的Value类型可以用来读取和修改任何类型的值。

类型*Value有几个方法:LoadStoreSwapCompareAndSwap(其中后两个方法实在Go 1.17中引入的)。 这些方法均以interface{}做为参数类型,所以传递给它们的实参可以是任何类型的值。 但是对于一个可寻址的Value类型的值v,一旦v.Store方法((&v).Store的简写形式)被曾经调用一次,则传递给值v的后续方法调用的实参的具体类型必须和传递给它的第一次调用的实参的具体类型一致; 否则,将产生一个恐慌。nil接口类型实参也将导致v.Store()方法调用产生恐慌。

一个例子:

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") // 将导致一个恐慌
}

另一个例子(针对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}}
}

事实上,我们也可以使用上一节介绍的指针原子操作来对任何类型的值进行原子读取和修改,不过需要多一级指针的间接引用。 两种方法有各自的好处和缺点。在实践中需要根据具体需要选择合适的方法。

原子操作相关的内存顺序保证

为了便于理解和使用简单,Go值的原子操作被设计的和内存顺序保证无关。 没有任何官方文档规定了原子操作应该保证的内存顺序。 详见Go中的内存顺序保证一文对此情况的说明。


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号