在咱们前面的一些介绍 sync 包相关的文章中,咱们应该也发现了,其中有不少当地运用了原子操作 比方 sync.WaitGroupsync.Map 再到 sync.Pool,这些结构体的完结中都有原子操作的身影。 原子操作在并发编程中是一种十分重要的操作,它能够确保并发安全,并且功率也很高。 本文将会深入探讨一下 go 中原子操作的原理、运用场景、用法等内容。

什么是原子操作?

原子操作是变量等级的互斥锁。

假如让我用一句话来阐明什么是原子操作,那便是:原子操作是变量等级的互斥锁。 简略来说,便是同一时刻,只能有一个 CPU 对变量进行读或写。 当咱们想要对某个变量做并发安全的修正,除了运用官方供给的 Mutex,还能够运用 sync/atomic 包的原子操作, 它能够确保对变量的读取或修正期间不被其他的协程所影响。

咱们能够用下图来表明:

深入理解 go 原子操作

阐明:在上图中,咱们有三个 CPU 逻辑核,其中 CPU 1 正在对变量 v 做原子操作,这个时分 CPU 2 和 CPU 3 不能对 v 做任何操作, 在 CPU 1 操作完结后,CPU 2 和 CPU 3 能够获取到 v 的最新值。

从这个视点看,咱们能够把 sync/atomic 包中的原子操作看成是变量等级的互斥锁。 便是说,在 go 中,当一个协程对变量做原子操作时,其他协程不能对这个变量做任何操作,直到这个协程操作完结。

原子操作的运用场景是什么?

拿一个简略的比方来阐明一下原子操作的运用场景:

func TestAtomic(t *testing.T) {
	var sum = 0
	var wg sync.WaitGroup
	wg.Add(1000)
	// 发动 1000 个协程,每个协程对 sum 做加法操作
	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			sum++
		}()
	}
	// 等候一切的协程都履行结束
	wg.Wait()
	fmt.Println(sum) // 这里输出多少呢?
}

咱们能够在自己的电脑上运转一下这段代码,看看输出的成果是多少。 不出意外的话,应该每次或许都不相同,并且应该也不是 1000,这是为什么呢?

这是由于,CPU 在对 sum 做加法的时分,需求先将 sum 目前的值读取到 CPU 的寄存器中,然后再进行加法操作,终究再写回到内存中。 假如有两个 CPU 一同取了 sum 的值,然后都进行了加法操作,然后都再写回到内存中,那么就会导致 sum 的值被掩盖,然后导致成果不正确。

举个比方,目前内存中的 sum 为 1,然后两个 CPU 一同取了这个 1 来做加法,然后都得到了成果 2, 然后这两个 CPU 将各自的核算成果写回到内存中,那么内存中的 sum 就变成了 2,而不是 3。

在这种场景下,咱们能够运用原子操作来完结并发安全的加法操作:

func TestAtomic1(t *testing.T) {
	// 将 sum 的类型改成 int32,由于原子操作只能针对 int32、int64、uint32、uint64、uintptr 这几种类型
	var sum int32 = 0
	var wg sync.WaitGroup
	wg.Add(1000)
    // 发动 1000 个协程,每个协程对 sum 做加法操作
	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			// 将 sum++ 改成下面这样
			atomic.AddInt32(&sum, 1)
		}()
	}
	wg.Wait()
	fmt.Println(sum) // 输出 1000
}

在上面这个比方中,咱们每次履行都能得到 1000 这个成果。

由于运用原子操作的时分,同一时刻只能有一个 CPU 对变量进行读或写,所以就不会呈现上面的问题了。

所以许多需求对变量做并发读写的当地,咱们都能够考虑一下,是否能够运用原子操作来完结并发安全的操作(而不是运用互斥锁,互斥锁功率比较原子操作要低一些)。

原子操作的运用场景也是和互斥锁类似的,可是不相同的是,咱们的锁粒度只是一个变量罢了。也便是说,当咱们不答应多个 CPU 一同对变量进行读写的时分(确保变量同一时刻只能一个 CPU 操作),就能够运用原子操作。

原子操作是怎样完结的?

看完上面原子操作的介绍,有没有觉得原子操作很神奇,居然有这么好用的东西。那它到底是怎样完结的呢?

一般情况下,原子操作的完结需求特殊的 CPU 指令或许体系调用。 这些指令或许体系调用能够确保在履行期间不会被其他操作或事件中止,然后确保操作的原子性。

例如,在 x86 架构的 CPU 中,能够运用 LOCK 前缀来完结原子操作。 LOCK 前缀能够与其他指令一同运用,用于确定内存总线,避免其他 CPU 拜访同一内存地址,然后完结原子操作。 在运用 LOCK 前缀的指令履行期间,CPU 会将当时处理器缓存中的数据写回到内存中,并确定该内存地址, 避免其他 CPU 修正该地址的数据(所以原子操作总是能够读取到最新的数据)。 一旦当时 CPU 对该地址的操作完结,CPU 会开释该内存地址的确定,其他 CPU 才干持续对该地址进行拜访。

x86 LOCK 的时分发生了什么

咱们再来捋一下上面的内容,看看 LOCK 前缀是怎么完结原子操作的:

  1. CPU 会将当时处理器缓存中的数据写回到内存中。(因此咱们总能读取到最新的数据)
  2. 然后确定该内存地址,避免其他 CPU 修正该地址的数据。
  3. 一旦当时 CPU 对该地址的操作完结,CPU 会开释该内存地址的确定,其他 CPU 才干持续对该地址进行拜访。

其他架构的 CPU 或许会略有不同,可是原理是相同的。

原子操作有什么特征?

  1. 不会被中止:原子操作是一个不可分割的操作,要么全部履行,要么全部不履行,不会呈现中间状态。这是确保原子性的根本前提。一同,原子操作进程中不会有上下文切换的进程。
  2. 操作目标是同享变量:原子操作通常是对同享变量进行的,也便是说,多个协程能够一同拜访这个变量,因此需求采用原子操作来确保数据的共同性和正确性。
  3. 并发安全:原子操作是并发安全的,能够确保多个协程一同进行操作时不会呈现数据竞赛问题(虽然说是一同,可是实际上在操作那个变量的时分是互斥的)。
  4. 无需加锁:原子操作不需求运用互斥锁来确保数据的共同性和正确性,因此能够避免互斥锁的运用带来的功用丢失。
  5. 适用场景比较限制:原子操作适用于操作单个变量,假如需求一同并发读写多个变量,或许需求考虑运用互斥锁。

go 里边有哪些原子操作?

在 go 中,主要有以下几种原子操作:AddCompareAndSwapLoadStoreSwap

增减(Add)

  1. 用于进行添加或减少的原子操作,函数名以 Add 为前缀,后缀针对特定类型的名称。
  2. 原子增被操作的类型只能是数值类型,即 int32int64uint32uint64uintptr
  3. 原子增减函数的第一个参数为原值,第二个参数是要增减多少。
  4. 办法:
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

int32int64 的第二个参数能够是负数,这样就能够做原子减法了。

比较并交流(CompareAndSwap)

也便是咱们常见的 CAS,在 CAS 操作中,会需求拿旧的值跟 old 比较,假如持平,就将 new 赋值给 addr。 假如不持平,则不做任何操作。终究回来一个 bool 值,表明是否成功 swap

也便是说,这个操作或许是不成功的。这很正常,在并发环境下,多个协程对同一个变量进行操作,必定会存在竞赛的情况。 在这种情况下,偶尔的失利是正常的,咱们只需求在失利的时分,从头测验即可。 由于原子操作需求的时刻往往是比较短的,因此在失利的时分,咱们能够经过自旋的办法来再次进行测验。

在这种情况下,假如不自旋,那就需求将这个协程挂起,等候其他协程完结操作,然后再次测验。这个进程比较自旋或许会更加耗时。 由于很有或许这次原子操作不成功,下一次就成功了。假如咱们每次都将协程挂起,那么功率就会大大下降。

for + 原子操作的办法,在 go 的 sync 包中许多当地都有运用,比方 sync.Mapsync.Pool 等。 这也是运用原子操作时一个十分常见的运用模式。

CompareAndSwap 的功用:

  1. 用于比较并交流的原子操作,函数名以 CompareAndSwap 为前缀,后缀针对特定类型的名称。
  2. 原子比较并交流被操作的类型能够是数值类型或指针类型,即 int32int64uint32uint64uintptrunsafe.Pointer
  3. 原子比较并交流函数的第一个参数为原值指针,第二个参数是要比较的值,第三个参数是要交流的值。
  4. 办法:
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

载入(Load)

原子性的读取操作接受一个对应类型的指针值,回来该指针指向的值。原子性读取意味着读取值的一同,当时核算机的任何 CPU 都不会进行针对值的读写操作。

假如不运用原子 Load,当运用 v := value 这种赋值办法为变量 v 赋值时,读取到的 value 或许不是最新的,由于在读取操作时其他协程对它的读写操作或许会一同发生。

Load 操作有下面这些:

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

存储(Store)

Store 能够将 val 值保存到 *addr 中,Store 操作是原子性的,因此在履行 Store 操作时,当时核算机的任何 CPU 都不会进行针对 *addr 的读写操作。

  1. 原子性存储会将 val 值保存到 *addr 中。
  2. 与读操作对应的写入操作,sync/atomic 供给了与原子值载入 Load 函数相对应的原子值存储 Store 函数,原子性存储函数均以 Store 为前缀。

Store 操作有下面这些:

func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintpre, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

交流(Swap)

SwapStore 有点类似,可是它会回来 *addr 的旧值。

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

原子操作恣意类型的值 – atomic.Value

从上一节中,咱们知道了在 go 中原子操作能够操作 int32int64uint32uint64uintptrunsafe.Pointer 这些类型的值。 可是在实际开发中,咱们的类型还有许多,比方 stringstruct 等等,那这些类型的值怎么进行原子操作呢?答案是运用 atomic.Value

atomic.Value 是一个结构体,它的内部有一个 any 类型的字段,存储了咱们要原子操作的值,也便是一个恣意类型的值。

atomic.Value 支持以下操作:

  • Load:原子性的读取 Value 中的值。
  • Store:原子性的存储一个值到 Value 中。
  • Swap:原子性的交流 Value 中的值,回来旧值。
  • CompareAndSwap:原子性的比较并交流 Value 中的值,假如旧值和 old 持平,则将 new 存入 Value 中,回来 true,不然回来 false

atomic.Value 的这些操作跟上面讲到的那些操作其实差不多,只不过 atomic.Value 能够操作恣意类型的值。 那 atomic.Value 是怎么完结的呢?

atomic.Value 源码剖析

atomic.Value 是一个结构体,这个结构体只要一个字段:

// Value 供给共同类型值的原子加载和存储。
type Value struct {
	v any
}

Load – 读取

Load 回来由最近的 Store 设置的值。假如还没有 Store 过任何值,则回来 nil

// Load 回来由最近的 Store 设置的值。
func (v *Value) Load() (val any) {
	// atomic.Value 转换为 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))
	// 判别 atomic.Value 的类型
	typ := LoadPointer(&vp.typ)
	// 第一次 Store 还没有完结,直接回来 nil
	if typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) {
		// firstStoreInProgress 是一个特殊的变量,存储到 typ 中用来表明第一次 Store 还没有完结
		return nil
	}
	// 获取 atomic.Value 的值
	data := LoadPointer(&vp.data)
	// 将 val 转换为 efaceWords 类型
	vlp := (*efaceWords)(unsafe.Pointer(&val))
	// 别离赋值给 val 的 typ 和 data
	vlp.typ = typ
	vlp.data = data
	return
}

atomic.Value 的源码中,咱们都能够看到 efaceWords 的身影,它实际上代表的是 interface{}/any 类型:

// 表明一个 interface{}/any 类型
type efaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

看到这里咱们会不会觉得很困惑,直接回来 val 不就能够了吗?为什么要将 val 转换为 efaceWords 类型呢?

这是由于 go 中的原子操作只能操作 int32int64uint32uint64uintptrunsafe.Pointer 这些类型的值, 不支持 interface{} 类型,可是假如了解 interface{} 底层结构的话,咱们就知道 interface{} 底层其实便是一个结构体, 它有两个字段,一个是 type,一个是 datatype 用来存储 interface{} 的类型,data 用来存储 interface{} 的值。 并且这两个字段都是 unsafe.Pointer 类型的,所以其实咱们能够对 interface{}typedata 别离进行原子操作, 这样终究其实也能够达到了原子操作 interface{} 的意图了,是不是十分地奇妙呢?

Store – 存储

StoreValue 的值设置为 val。对给定值的一切存储调用有必要运用相同详细类型的值。不共同类型的存储会发生惊惧,Store(nil) 也会 panic

// Store 将 Value 的值设置为 val。
func (v *Value) Store(val any) {
	// 不能存储 nil 值
	if val == nil {
		panic("sync/atomic: store of nil value into Value")
	}
	// atomic.Value 转换为 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))
	// val 转换为 efaceWords
	vlp := (*efaceWords)(unsafe.Pointer(&val))
	// 自旋进行原子操作,这个进程不会好久,开支比较互斥锁小
	for {
		// LoadPointer 能够确保获取到的是最新的
		typ := LoadPointer(&vp.typ)
		// 第一次 store 的时分 typ 仍是 nil,阐明是第一次 store
		if typ == nil {
			// 测验开端第一次 Store。
			// 禁用抢占,以便其他 goroutines 能够自旋等候完结。
			// (假如答应抢占,那么其他 goroutine 自旋等候的时刻或许会比较长,由于或许会需求进行协程调度。)
			runtime_procPin()
			// 抢占失利,意味着有其他 goroutine 成功 store 了,答应抢占,再次测验 Store
			// 这也是一个原子操作。
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				runtime_procUnpin()
				continue
			}
			// 完结第一次 store
			// 由于有 firstStoreInProgress 标识的维护,所以下面的两个原子操作是安全的。
			StorePointer(&vp.data, vlp.data) // 存储值(原子操作)
			StorePointer(&vp.typ, vlp.typ)   // 存储类型(原子操作)
			runtime_procUnpin()              // 答应抢占
			return
		}
		// 别的一个 goroutine 正在进行第一次 Store。自旋等候。
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			continue
		}
		// 第一次 Store 现已完结了,下面不是第一次 Store 了。
		// 需求查看当时 Store 的类型跟第一次 Store 的类型是否共同,不共同就 panic。
		if typ != vlp.typ {
			panic("sync/atomic: store of inconsistently typed value into Value")
		}
		// 后续的 Store 只需求 Store 值部分就能够了。
		// 由于 atomic.Value 只能保存一种类型的值。
		StorePointer(&vp.data, vlp.data)
		return
	}
}

Store 中,有以下几个留意的点:

  1. 运用 firstStoreInProgress 来确保第一次 Store 的时分,只要一个 goroutine 能够进行 Store 操作,其他的 goroutine 需求自旋等候。假如没有这个维护,那么存储 typdata 的时分就会呈现竞赛(由于需求两个原子操作),导致数据不共同。在这里其实能够将 firstStoreInProgress 看作是一个互斥锁。
  2. 在进行第一次 Store 的时分,会将当时的 goroutine 和 P 绑定,这样拿到 firstStoreInProgress 锁的协程就能够尽快地完结第一次 Store 操作,这样一来,其他的协程也不用等候太久。
  3. 在第一次 Store 的时分,会有两个原子操作,别离存储类型和值,可是由于有 firstStoreInProgress 的维护,所以这两个原子操作本质上是对 interface{} 的一个原子存储操作。
  4. 其他协程在看到有 firstStoreInProgress 标识的时分,就会自旋等候,直到第一次 Store 完结。
  5. 在后续的 Store 操作中,只需求存储值就能够了,由于 atomic.Value 只能保存一种类型的值。

Swap – 交流

SwapValue 的值设置为 new 并回来旧值。对给定值的一切交流调用有必要运用相同详细类型的值。一同,不共同类型的交流会发生惊惧,Swap(nil) 也会 panic

// Swap 将 Value 的值设置为 new 并回来旧值。
func (v *Value) Swap(new any) (old any) {
	// 不能存储 nil 值
	if new == nil {
		panic("sync/atomic: swap of nil value into Value")
	}
	// atomic.Value 转换为 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))
	// new 转换为 efaceWords
	np := (*efaceWords)(unsafe.Pointer(&new))
	// 自旋进行原子操作,这个进程不会好久,开支比较互斥锁小
	for {
		// 下面这部分代码跟 Store 相同,不细说了。
		// 这部分代码是进行第一次存储的代码。
		typ := LoadPointer(&vp.typ)
		if typ == nil {
			runtime_procPin()
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				runtime_procUnpin()
				continue
			}
			StorePointer(&vp.data, np.data)
			StorePointer(&vp.typ, np.typ)
			runtime_procUnpin()
			return nil
		}
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			continue
		}
		if typ != np.typ {
			panic("sync/atomic: swap of inconsistently typed value into Value")
		}
		// ---- 下面是 Swap 的特有逻辑 ----
		// op 是回来值
		op := (*efaceWords)(unsafe.Pointer(&old))
		// 回来旧的值
		op.typ, op.data = np.typ, SwapPointer(&vp.data, np.data)
		return old
	}
}

CompareAndSwap – 比较并交流

CompareAndSwapValue 的值与 old 比较,假如持平则设置为 new 并回来 true,不然回来 false。 对给定值的一切比较和交流调用有必要运用相同详细类型的值。一同,不共同类型的比较和交流会发生惊惧,CompareAndSwap(nil, nil) 也会 panic

// CompareAndSwap 比较并交流。
func (v *Value) CompareAndSwap(old, new any) (swapped bool) {
	// 留意:old 是能够为 nil 的,new 不能为 nil。
	// old 是 nil 表明是第一次进行 Store 操作。
	if new == nil {
		panic("sync/atomic: compare and swap of nil value into Value")
	}
	// atomic.Value 转换为 efaceWords
	vp := (*efaceWords)(unsafe.Pointer(v))
	// new 转换为 efaceWords
	np := (*efaceWords)(unsafe.Pointer(&new))
	// old 转换为 efaceWords
	op := (*efaceWords)(unsafe.Pointer(&old))
	// old 和 new 类型有必要共同,且不能为 nil
	if op.typ != nil && np.typ != op.typ {
		panic("sync/atomic: compare and swap of inconsistently typed values")
	}
	// 自旋进行原子操作,这个进程不会好久,开支比较互斥锁小
	for {
		// LoadPointer 能够确保获取到的 typ 是最新的
		typ := LoadPointer(&vp.typ)
		if typ == nil { // atomic.Value 是 nil,还没 Store 过
			// 准备进行第一次 Store,可是传递进来的 old 不是 nil,compare 这一步就失利了。直接回来 false
			if old != nil {
				return false
			}
			// 下面这部分代码跟 Store 相同,不细说了。 
			// 这部分代码是进行第一次存储的代码。
			runtime_procPin()
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
				runtime_procUnpin()
				continue
			}
			StorePointer(&vp.data, np.data)
			StorePointer(&vp.typ, np.typ)
			runtime_procUnpin()
			return true
		}
		if typ == unsafe.Pointer(&firstStoreInProgress) {
			continue
		}
		if typ != np.typ {
			panic("sync/atomic: compare and swap of inconsistently typed value into Value")
		}
		// 经过运转时持平性查看比较旧版别和当时版别。
		// 这答应对值类型进行比较,这是包函数所没有的。
		// 下面的 CompareAndSwapPointer 仅确保 vp.data 自 LoadPointer 以来没有更改。
		data := LoadPointer(&vp.data)
		var i any
		(*efaceWords)(unsafe.Pointer(&i)).typ = typ
		(*efaceWords)(unsafe.Pointer(&i)).data = data
		if i != old { // atomic.Value 跟 old 不持平
			return false
		}
		// 只做 val 部分的 cas 操作
		return CompareAndSwapPointer(&vp.data, data, np.data)
	}
}

这里需求特别阐明的只要终究那个比较持平的判别,也便是 data := LoadPointer(&vp.data) 以及往后的几行代码。 在开发 atomic.Value 第一版的时分,那个开发者其实是将这几行写成 CompareAndSwapPointer(&vp.data, old.data, np.data) 这种形式的。 可是在旧的写法中,会存在一个问题,假如咱们做 CAS 操作的时分,假如传递的参数 old 是一个结构体的值这种类型,那么这个结构体的值是会被拷贝一份的, 一同再会被转换为 interface{}/any 类型,这个进程中,其实参数的 olddata 部分指针指向的内存跟 vp.data 指向的内存是不相同的。 这样的话,CAS 操作就会失利,这个时分就会回来 false,可是咱们本意是要比较它的值,呈现这种成果显然不是咱们想要的。

将值作为 interface{} 参数运用的时分,会存在一个将值转换为 interface{} 的进程。详细咱们能够看看 interface{} 的完结原理。

所以,在上面的完结中,会将旧值的 typdata 赋值给一个 any 类型的变量, 然后运用 i != old 这种办法进行判别,这样就能够完结在比较的时分,比较的是值,而不是由值转换为 interface{} 后的指针。

其他原子类型

咱们现在知道了,atomic.Value 能够对恣意类型做原子操作。 而关于其他的原子类型,比方 int32int64uint32uint64uintptrunsafe.Pointer 等, 其实在 go 中也供给了包装的类型,让咱们能够以目标的办法来操作这些类型。

对应的类型如下:

  • atomic.Bool:这个比较特别,但底层实际上是一个 uint32 类型的值。咱们对 atomic.Bool 做原子操作的时分,实际上是对 uint32 做原子操作。
  • atomic.Int32int32 类型的包装类型
  • atomic.Int64int64 类型的包装类型
  • atomic.Uint32uint32 类型的包装类型
  • atomic.Uint64uint64 类型的包装类型
  • atomic.Uintptruintptr 类型的包装类型
  • atomic.Pointerunsafe.Pointer 类型的包装类型

这几种类型的完结的代码根本相同,除了类型不相同,咱们能够看看 atomic.Int32 的完结:

// An Int32 is an atomic int32. The zero value is zero.
type Int32 struct {
	_ noCopy
	v int32
}
// Load atomically loads and returns the value stored in x.
func (x *Int32) Load() int32 { return LoadInt32(&x.v) }
// Store atomically stores val into x.
func (x *Int32) Store(val int32) { StoreInt32(&x.v, val) }
// Swap atomically stores new into x and returns the previous value.
func (x *Int32) Swap(new int32) (old int32) { return SwapInt32(&x.v, new) }
// CompareAndSwap executes the compare-and-swap operation for x.
func (x *Int32) CompareAndSwap(old, new int32) (swapped bool) {
	return CompareAndSwapInt32(&x.v, old, new)
}

能够看到,atomic.Int32 的完结都是基于 atomic 包中 int32 类型相关的原子操作函数来完结的。

原子操作与互斥锁比较

那咱们有了互斥锁,为什么还要有原子操作呢?咱们进行比较一下就知道了:

原子操作 互斥锁
维护的规模 变量 代码块
维护的粒度
功用
怎么完结的 硬件指令 软件层面完结,逻辑较多

假如咱们只需求对某一个变量做并发读写,那么运用原子操作就能够了,由于原子操作的功用比互斥锁高许多。 可是假如咱们需求对多个变量做并发读写,那么就需求用到互斥锁了,这种场景往往是在一段代码中对不同变量做读写。

功用比较

咱们前面这个表格提到了原子操作与互斥锁功用上有差异,咱们写几行代码来进行比较一下:

// 体系信息 cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
// 10.13 ns/op
func BenchmarkMutex(b *testing.B) {
   var mu sync.Mutex
   for i := 0; i < b.N; i++ {
      mu.Lock()
      mu.Unlock()
   }
}
// 5.849 ns/op
func BenchmarkAtomic(b *testing.B) {
   var sum atomic.Uint64
   for i := 0; i < b.N; i++ {
      sum.Add(uint64(1))
   }
}

在对 Mutex 的功用测验中,我只是写了简略的 Lock()UnLock() 操作,由于这种比较才算是对 Mutex 自身的测验,而在 Atomic 的功用测验中,对 sum 做原子累加的操作。终究成果是,运用 Atomic 的操作耗时大约比 Mutex 少了 40% 以上。

在实际开发中,Mutex 维护的临界区内往往有更多操作,也就意味着 Mutex 锁需求消耗更长的时刻才干开释,也便是会需求消耗比上面这个 40% 还要多的时刻别的一个协程才干获取到 Mutex 锁。

go 的 sync 包中的原子操作

在文章的开头,咱们就说了,在 go 的 sync.Mapsync.Pool 中都有用到了原子操作,本节就来看一看这些操作。

sync.Map 中的原子操作

sync.Map 中运用到了一个 entry 结构体,这个结构体中大部分操作都是原子操作,咱们能够看看它下面这两个办法的界说:

// 删去 entry
func (e *entry) delete() (value any, ok bool) {
	for {
		p := e.p.Load()
		// 现已被删去了,不需求再删去
		if p == nil || p == expunged {
			return nil, false
		}
		// 删去成功
		if e.p.CompareAndSwap(p, nil) {
			return *p, true
		}
	}
}
// 假如条目尚未删去,trySwap 将交流一个值。
func (e *entry) trySwap(i *any) (*any, bool) {
	for {
		p := e.p.Load()
		// 现已被删去了
		if p == expunged {
			return nil, false
		}
		// swap 成功
		if e.p.CompareAndSwap(p, i) {
			return p, true
		}
	}
}

咱们能够看到一个十分典型的特征便是 for + CompareAndSwap 的组合,这个组合在 entry 中呈现了许屡次。

假如咱们也需求对变量做并发读写,也能够测验一下这种 for + CompareAndSwap 的组合。

sync.WaitGroup 中的原子操作

sync.WaitGroup 中有一个类型为 atomic.Uint64state 字段,这个变量是用来记载 WaitGroup 的状态的。 在实际运用中,它的高 32 位用来记载 WaitGroup 的计数器,低 32 位用来记载 WaitGroupWaiter 的数量,也便是等候条件变量满意的协程数量。

假如不运用一个变量来记载这两个值,那么咱们就需求运用两个变量来记载,这样就会导致咱们需求对两个变量做并发读写, 在这种情况下,咱们就需求运用互斥锁来维护这两个变量,这样就会导致功用的下降。

而运用一个变量来记载这两个值,咱们就能够运用原子操作来维护这个变量,这样就能够确保并发读写的安全性,一同也能得到更好的功用:

// WaitGroup 的 Add 函数:高 32 位加上 delta
state := wg.state.Add(uint64(delta) << 32)
// WaitGroup 的 Wait 函数:低 32 位加 1
// 等候者的数量加 1
wg.state.CompareAndSwap(state, state+1)

CAS 操作有失利必定有成功

当然这里是指指向同一行 CAS 代码的时分(也便是有竞赛的时分),假如是指向不同行 CAS 代码的时分,那么就不一定了。 比方下面这个比方,咱们把前面核算 sum 的比方改一改,改成用 CAS 操作来完结:

func TestCas(t *testing.T) {
	var sum int32 = 0
	var wg sync.WaitGroup
	wg.Add(1000)
	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			// 这一行是有或许会失利的
			atomic.CompareAndSwapInt32(&sum, sum, sum+1)
		}()
	}
	wg.Wait()
	fmt.Println(sum) // 不是 1000
}

在这个比方中,咱们把 atomic.AddInt32(&sum, 1) 改成了 atomic.CompareAndSwapInt32(&sum, sum, sum+1), 这样就会导致有或许会有多个 goroutine 一同履行到 atomic.CompareAndSwapInt32(&sum, sum, sum+1) 这一行代码, 这样必定会有不同的 goroutine 一同拿到一个相同的 sum 的旧值,那么在这种情况下,就会导致 CAS 操作失利。 也便是说,将 sum 替换为 sum + 1 的操作或许会失利。

失利意味着什么呢?意味着别的一个协程序先把 sum 的值加 1 了,这个时分其实咱们不该该在旧的 sum 上加 1 了, 而是应该在最新的 sum 上加上 1,那咱们应该怎样做呢?咱们能够在 CAS 操作失利的时分,从头获取 sum 的值, 然后再次测验 CAS 操作,直到成功为止:

func TestCas(t *testing.T) {
	var sum int32 = 0
	var wg sync.WaitGroup
	wg.Add(1000)
	for i := 0; i < 1000; i++ {
		go func() {
			defer wg.Done()
			// cas 失利的时分,从头获取 sum 的值进行核算。
			// cas 成功则回来。
			for {
				if atomic.CompareAndSwapInt32(&sum, sum, sum+1) {
					return
				}
			}
		}()
	}
	wg.Wait()
	fmt.Println(sum)
}

总结

原子操作是并发编程中十分重要的一个概念,它能够确保并发读写的安全性,一同也能得到更好的功用。

终究,总结一下本文讲到的内容:

  • 原子操作是更加底层的操作,它维护的是单个变量,而互斥锁能够维护一个代码片段,它们的运用场景是不相同的。
  • 原子操作需求经过 CPU 指令来完结,而互斥锁是在软件层面完结的。
  • go 里边的原子操作有以下这些:
    • Add:原子增减
    • CompareAndSwap:原子比较并交流
    • Load:原子读取
    • Store:原子写入
    • Swap:原子交流
  • go 里边一切类型都能运用原子操作,只是不同类型的原子操作运用的函数不太相同。
  • atomic.Value 能够用来原子操作恣意类型的变量。
  • go 里边有些底层完结也运用了原子操作,比方:
    • sync.WaitGroup:运用原子操作来确保计数器和等候者数量的并发读写安全性。
    • sync.Mapentry 结构体中根本一切操作都有原子操作的身影。
  • 原子操作有失利必定有成功(说的是同一行 CAS 操作),假如 CAS 操作失利了,那么咱们能够从头获取旧值,然后再次测验 CAS 操作,直到成功为止。

总的来说,原子操作自身其实没有太杂乱的逻辑,咱们理解了它的原理之后,就能够很容易的运用它了。