[Go并发]原子操作5种姿势

155 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

🎐 放在前面说的话

大家好,我是沥沥樱 👧🏻

本科在读,此为日常捣鼓.

如有不对,请多指教,也欢迎大家来跟我讨论鸭 👏👏👏

还有还有还有很重要的,麻烦大可爱们动动小手,给沥沥樱点颗心心♥,沥沥樱需要鼓励嗷呜~

今天我们来看一下原子操作5种姿势

Let’s get it!

原子操作概述

基本概念

原子性:一个或多个操作在CPU的执行过程中不被中断的特性,称为原子性。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。

原子操作:进行过程中不能被中断的操作

原子操作&锁比较

  • 原子操作由底层硬件支持,而锁则是由操作系统提供的API实现,原子操作比锁更为高效。
  • 加锁比较耗时,需要上下文切换。即使是goroutine也需要上下文切换
  • 只针对基本类型,可使用原子操作保证线程安全
  • 原子操作在用户态完成,性能比互斥锁要高
  • 原子操作步骤简单,不需要加锁-操作-解锁

五种操作函数

  • 增或减 (Add)
  • 比较并交换 (CAS, Compare And Swap)
  • 载入 (Load)
  • 存储 (Store)
  • 交换 (Swap)

增或减(Add)

函数名称都以Add为前缀,并后跟针对的具体类型的名称。

  • 被操作的类型只能是数值类型
  • int32,int64,uint32,uint64,uintptr类型可以使用原子增或减操作
  • 第一个参数值必须是一个指针类型的值,以便施加特殊的CPU指令
  • 第二个参数值的类型和第一个被操作值的类型总是相同的。
func main() {
	var i32 int32
	fmt.Println("=====old i32 value=====")
	fmt.Println(i32)
	//第一个参数值必须是一个指针类型的值,因为该函数需要获得被操作值在内存中的存放位置,以便施加特殊的CPU指令
	//结束时会返回原子操作后的新值
	for i := 0; i < 3; i++ {
		newI32 := atomic.AddInt32(&i32, 1)
		//newI32 := atomic.AddInt64(&i32, -1)
		fmt.Println("=====new i32 value=====")
		fmt.Println(i32)
		fmt.Println(newI32)
	}
}

打印:
=====old i32 value=====
0
=====new i32 value=====
1
1
=====new i32 value=====
2
2
=====new i32 value=====
3
3

比较并交换 (CAS, Compare & Swap)

以‘CompareAndSwap’为前缀的若干个函数代表。

  • 声明如下
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
  • 仍然保持参数 old 所记录的值,满足此前提下才进行交换操作,否则操作就会被忽略。
  • 当有大量 goroutine 对变量进行读写操作时,可能导致CAS操作无法成功,需要用for循环不断进行尝试,直到成功为止
var value int32

func main() {
	fmt.Println("======old value=======")
	fmt.Println(value)
	fmt.Println("======CAS value=======")
	addValue(2)
	fmt.Println(value)

}

// 不断地尝试原子地更新value的值,直到操作成功为止
func addValue(delta int32) {

	// 操作值被频繁变更的情况下,CAS操作不容易成功,需要for循环以进行多次尝试
	for {
		v := value
		if atomic.CompareAndSwapInt32(&value, v, (v + delta)) {
			//在函数的结果值为true时,退出循环
			break
		}
		//操作失败的缘由总会是value的旧值已不与v的值相等了.
	}
}

打印:
======old value=======
0
======CAS value=======
2

载入 (Load)

上面的比较并交换案例总 v:= value为变量v赋值,但… 要注意,在进行读取value的操作的过程中,其他对此值的读写操作是可以被同时进行的,那么这个读操作很可能会读取到一个只被修改了一半的数据。所以,我们需要使用Load为前缀(载入),来确保这样的糟糕事情发生。

  • atomic.LoadInt32接受一个*int32类型的指针值
  • 返回该指针指向的那个值
var value int32

func main() {
	fmt.Println("======old value=======")
	fmt.Println(value)
	fmt.Println("======CAS value=======")
	addValue(2)
	fmt.Println(value)

}

//不断地尝试原子地更新value的值,直到操作成功为止
func addValue(delta int32) {
	// 操作值被频繁变更的情况下,CAS操作不容易成功,需要for循环以进行多次尝试
	for {
		//v := value
		// 使用载入防止读取到一个只被修改了一半的数据
		v := atomic.LoadInt32(&value)
		if atomic.CompareAndSwapInt32(&value, v, (v + delta)) {
			//在函数的结果值为true时,退出循环
			break
		}
		//操作失败的缘由总会是value的旧值已不与v的值相等了.
	}
}

打印同上

存储 (Store)

与原子的载入函数相对应的原子的值存储函数。,以Store为前缀

  • 在原子地存储某个值的过程中,任何CPU都不会进行针对同一个值的读或写操作。
  • 原子的值存储操作总会成功,因为它并不会关心被操作值的旧值是什么
  • 和CAS操作有着明显的区别
var value int32

func main() {
	fmt.Println("======old value=======")
	fmt.Println(value)
	fmt.Println("======Store value=======")
	atomic.StoreInt32(&value, 1)
	fmt.Println(value)

}

打印:
======old value=======
0
======Store value=======
1

交换 (Swap)

  • 与CAS操作不同,原子交换操作不会关心被操作的旧值。
  • 它会直接设置新值
  • 它会返回被操作值的旧值
  • 此类操作比CAS操作的约束更少,同时又比原子载入操作的功能更强

🎉 放在后面说的话

以上简单总结了Go并发中原子操作增或减 (Add)、比较并交换 (CAS, Compare And Swap)、载入 (Load)、存储 (Store)、交换 (Swap)的使用,其他后期补充