Go基础学习10-原子并发包sync.atomic的使用:CSA、Swap、atomic.Value......

时间:2024-10-03 14:45:40

文章目录

  • 互斥锁的使用是否能够保证绝对的原子性
  • Go语言的原子包sync.atomic
  • sync.Value使用
  • 互斥锁与原子类的关系以及代码中应该如何选择

互斥锁的使用是否能够保证绝对的原子性

在之前文章中介绍了sync.Mutex变量用于保证并发操作时数据安全性。但是否sync.Mutex变量能够保证操作的原子性。不能。对于单个语句本身是原子性的,只要执行到这条语句就一定是原子性的。对于sync.Mutex变量包裹的语句并非单条原子语句。
sync.Mutex变量能够保证一个 goroutine 在执行临界区中的代码时,不被其他的goroutine 打扰,Go语言中的运行时系统中的调度器,会协调不同goroutine,使得一定时刻只能运行一定数量的goroutine。 所以即使加了sync.Mutex修饰临界区,当前goroutine也可能在调度器的作用下中断执行,并让它由运行状态转为非运行状态。
互斥锁可以保证临界区中代码的串行执行,但却不能保证这些代码执行的原子性(atomicity)

Go语言的原子包sync.atomic

在原子包sync.atomic中的函数可以做的原子操作有:

  • 加法(add)

  • 比较并交换(compare and swap:CAS)

  • 加载(Load)

  • 存储(stroe)

  • 交换(swap)

    对于上述原子操作类型中的每一个,可以支持的数据类型有:==int32、int64、uint32、uint64、uintptr,以及unsafe包中的Pointer。==不过,针对unsafe.Pointer类型,该包并未提供进行原子加法操作的函数。

下面以atomic.Addint32以及atomic.AddUint32为例编写代码如下:

// ------------------------------------------------------------
// package atomic
// @file      : demo01.go
// @author    : WeiTao
// @contact   : 15537588047@163.com
// @time      : 2024/10/2 20:44
// ------------------------------------------------------------
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	a := int32(0)
	a = atomic.AddInt32(&a, int32(23))
	fmt.Println(a)
	swapBool := atomic.CompareAndSwapInt32(&a, int32(23), int32(45))
	if swapBool {
		fmt.Printf("a swap val is : %v\n", a)
	}

	// 验证,通过AddUint32执行原子减法
	fmt.Println("===========================")
	b := uint32(18)
	delat := int32(-3)
	b = atomic.AddUint32(&b, uint32(delat))
	fmt.Println("b sub 3 result is : ", b)
	// 计算机系统中补码表示为:源码 求反 + 1.
	// ^uint32(-(-3) - 1)表示的补码与int32(-3)表示的补码相同的
	b = atomic.AddUint32(&b, ^uint32(-(-3)-1))
	fmt.Println("b sub 3 result is : ", b)
}

需要注意由于atomic.AddInt32()

// AddInt32 atomically adds delta to *addr and returns the new value.
// Consider using the more ergonomic and less error-prone [Int32.Add] instead.
func AddInt32(addr *int32, delta int32) (new int32)

将变量delta对应的值添加到指针addr地址对应的变量上,所以第一个类型必须传递指针,不能传递值,传递值的话就是拷贝一个副本,并不能对原变量值进行修改。

sync.Value使用

根据上一章节能够得知,对于go中原生的支持原子操作的函数支持的变量类型int32、int64、uint32、uint64、uintptr,以及unsafe包中的Pointer。为了弥补上述支持的类型变量的缺陷,go语言提供一个sync.Value类型。这个类型相当于一个容器,可以被用来原子地存储和加载任意的值。
atomic.Value类型是开箱即用的,我们声明一个该类型的变量(以下简称原子变量)之后就可以直接使用了。
atomic.Value类型变量提供四个操作:

// Load returns the value set by the most recent Store.
// It returns nil if there has been no call to Store for this Value.
func (v *Value) Load() (val any) 

// Store sets the value of the [Value] v to val.
// All calls to Store for a given Value must use values of the same concrete type.
// Store of an inconsistent type panics, as does Store(nil).
func (v *Value) Store(val any)

// Swap stores new into Value and returns the previous value. It returns nil if
// the Value is empty.
//
// All calls to Swap for a given Value must use values of the same concrete
// type. Swap of an inconsistent type panics, as does Swap(nil).
func (v *Value) Swap(new any) (old any)

// CompareAndSwap executes the compare-and-swap operation for the [Value].
//
// All calls to CompareAndSwap for a given Value must use values of the same
// concrete type. CompareAndSwap of an inconsistent type panics, as does
// CompareAndSwap(old, nil).
func (v *Value) CompareAndSwap(old, new any) (swapped bool)

根据上述函数定义可以得知atomic.Value变量使用时的一些限制:

  • 不能用原子值存储nil。也就是说,我们不能把nil作为参数值传入原子值的Store方法,否则就会引发一个panic。
  • 我们向原子值存储的第一个值,决定了它今后能且只能存储哪一个类型的值。
  • 交换操作和比较并交换操作均不能交换nil。
  • 当atomic.Value类型的变量被真正使用(存储值)后,他就不能再被复制了。

使用代码示例:

// ------------------------------------------------------------
// package main
// @file      : atomicValue.go
// @author    : WeiTao
// @contact   : 15537588047@163.com
// @time      : 2024/10/2 21:44
// ------------------------------------------------------------
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	// 创建atomic.Value类型的值
	box := atomic.Value{}
	// 错误示例:构建一个切片并直接存储
	 slice := []int{1, 2, 3}
	// box.Store(slice)
	fmt.Println(box.Load().([]int))
	fmt.Println(box.Load().([]int))

	// 正确的存储,由于切片是引用类型,使用方式一进行存储的话在另一个goroutine中也可以改变存储的底层的slice的值,使用下述方法
	// 直接创建一个本地的切片,并复制原有切片,指针和外界没有关系。
	store := func(v []int) {
		replica := make([]int, len(v))
		copy(replica, v)
		box.Store(replica)
	}
	store(slice)
	fmt.Println(box.Load().([]int))

	// 调用atomic.Value的Swap方法
	oldSlice := (box.Swap([]int{3, 5})).([]int)
	fmt.Printf("old slice: %v\n", oldSlice)
	fmt.Printf("new slice:  %v\n", box.Load().([]int))
	// 调用atomic.Value的CAS方法,下面的方法是实现不了的,由于切片是引用类型不支持比较
	// box.CompareAndSwap([]int{3, 5}, []int{9, 10})
	fmt.Println(box.Load().([]int))
}

注意:我上面对于切片的CAS操作是不正确的,Go语言中对于切片、函数、字典(map)都是引用类型,都不能进行比较,会引发panic。所以即使atomic.Value类型可以用于存储切片等引用类型,但尽量不要使用CAS操作。同时存储切片类型的话不能直接将切片类型直接存储到stomic.Value变量类型,由于切片是引用类型,在其他goroutine也可以更改其值。最好参考上述代码的方式构建一个副本并进行存储。

	slice := []int{1, 2, 3}
	store := func(v []int) {
		replica := make([]int, len(v))
		copy(replica, v)
		box.Store(replica)
	}
	store(slice)

互斥锁与原子类的关系以及代码中应该如何选择

  • 原子操作明显比互斥锁要更加轻便,但是限制也同样明显。所以,我们在进行二选一的时候通常不会太困难。
  • 原子值与互斥锁之间的选择有时候就需要仔细的考量了。

使用原子值尽量遵循如下几条原则:

  • 不要对外暴露原子变量、不要传递原子值及其指针值、尽量不要在原子值中存储引用类型的值。
  1. 不要把内部使用的原子值暴露给外界。比如,声明一个全局的原子变量并不是一个正确的做法。这个变量的访问权限最起码也应该是包级私有的。
  2. 如果不得不让包外,或模块外的代码使用你的原子值,那么可以声明一个包级私有的原子变量,然后再通过一个或多个公开的函数,让外界间接地使用到它。注意,这种情况下不要把原子值传递到外界,不论是传递原子值本身还是它的指针值。
  3. 如果通过某个函数可以向内部的原子值存储值的话,那么就应该在这个函数中先判断被存储值类型的合法性。若不合法,则应该直接返回对应的错误值,从而避免 panic 的发生。
  4. 如果可能的话,我们可以把原子值封装到一个数据类型中,比如一个结构体类型。这样,我们既可以通过该类型的方法更加安全地存储值,又可以在该类型中包含可存储值的合法类型信息。

以后会再补充一些关于原子操作以及原子值的更详细的讲解,本篇只能作为入门参考,后续深入学习后继续补充。