go atomic原子操作详细解读

文章目录

  • 概要
  • 1、基本知识
    • 1.1 原子操作是什么
    • 1.2 CPU怎么实现原子操作的?
  • 2、atomic包
    • 2.1、 Add函数
    • 2.2、CompareAndSwap函数
    • 2.3、Swap函数
    • 2.4、Load函数
    • 2.5、Store函数
  • 3、atomic.Value值

概要

atomic包是golang通过对底层系统支持的原子操作进行封装,而提供的原子操作包,用于实现无锁化并发安全的操作数据。支持加法(Add)、比较并交换(CompareAndSwap)、直接交换(Swap)、设置指针变量值(Store)、获取指针变量值(Load);还设置了atomic.Value结构,支持对象存储、获取、比较并交换、直接交换操作。

atmoic包怎么用?以加法为例,变量加法运算操作并不是原子性的,包括从内存中取值放入加法寄存器、寄存器运算得到结果、将结果写回内存,在并发环境下,一个协程的运算结果可能被另一个覆盖,从而导致丢失修改的问题,例如以下代码的最后打印结果不会是10000。

var wg sync.WaitGroup
var num = 0

func Add() {
	num++
	wg.Done()
}
func main() {
	size := 10000
	wg.Add(size)
	for i := 0; i < size; i++ {
		go Add()
	}
	wg.Wait()
	fmt.Println(num)
}

而以下代码时10000:

var wg sync.WaitGroup
var num int32 = 0

func Add() {
	atomic.AddInt32(&num, 1)
	wg.Done()
}
func main() {
	size := 10000
	wg.Add(size)
	for i := 0; i < size; i++ {
		go Add()
	}
	wg.Wait()
	fmt.Println(num)
}

1、基本知识

1.1 原子操作是什么

原子操作是指在执行过程中不会被中断的操作,要么全部执行完成,要么完全不执行,不存在部分执行的情况。原子操作可以看作是一个不可分割的单元,其他线程或进程无法在其中间进行干扰或插入。
原子操作通常用于对共享数据进行读取、写入和修改等操作,以保证数据的一致性和正确性。在多线程或多进程环境下,如果多个线程或进程同时访问和修改同一份共享数据,没有正确的同步机制或使用原子操作可能会导致数据竞争和不确定的行为。
常见的原子操作包括:

  • 原子读(Atomic Read):从内存中获取一个共享变量的值,并确保其他线程或进程不会在读取过程中修改该值。
  • 原子写(Atomic Write):将一个值写入到共享变量,并确保其他线程或进程不会在写入过程中读取或修改该值。
  • 原子加(Atomic Add):将一个特定的值与共享变量相加,并将结果存回共享变量中,确保其他线程或进程不会在加法过程中干扰。
  • 原子比较并交换(Atomic Compare-and-Swap):比较共享变量的当前值与预期值是否相等,如果相等,则将新值写入共享变量;如果不相等,则不做任何修改。该操作常用于实现同步机制和锁。

1.2 CPU怎么实现原子操作的?

现在的CPU一般为多核处理器,它底层实现原子操作的方式有多种,下面介绍几种常见的方法:

  1. 总线锁定:在多核处理器中,可以使用总线锁定机制来确保原子操作。当一个处理器需要执行原子操作时,它会向总线发送一个请求锁定的信号。其他处理器在接收到该信号后,将暂停对总线的访问,以防止干扰正在进行的原子操作。只有当原子操作完成并释放锁定时,其他处理器才能继续对总线进行访问。

  2. 缓存一致性协议:多核处理器中的每个核心通常都具有自己的缓存,这就需要确保各个核心之间的缓存数据的一致性。缓存一致性协议可以通过在多个核心之间共享和更新处理器**缓存行(缓存在cpu核心本地缓存中的一条数据)**的状态信息来实现原子操作。常见的缓存一致性协议有MESI,多个核心更新数据时需要遵循以下原则:

    • Modified(修改)状态:当一个核心修改了一个缓存行中的数据时,该缓存行将被标记为“修改”状态。此时,其他核心的缓存中对应的缓存行就被视为无效(Invalid),即无效数据。

    • Exclusive(独占)状态:当一个核心从内存中加载了一个缓存行到自己的缓存中,并且该缓存行在其他核心的缓存中没有副本时,该缓存行处于“独占”状态。这时,其他核心需要读取或写入该数据时,必须通过该核心进行访问,独占状态的数据,可以直接进行修改。

    • Shared(共享)状态:当多个核心都拥有同一个缓存行的副本时,该缓存行处于“共享”状态。此时,对于读操作,其他核心可以直接从自己的缓存中读取数据。对于写操作,需要通过总线发出命令让其他缓存的状态更新为“失效”,该缓存中数据更新为独占,才能修改。

    • Invalid(无效)状态:当一个核心修改了一个缓存行的数据时,该缓存行在其他核心的数据状态会被标记为“无效”状态。这意味着其他核心需要重新从内存中加载最新的数据。

      总的逻辑是,当一个核心读取内存中数据在自己的本地缓存中并且只有该核心的缓存拥有该缓存行,这个缓存行的状态为独占状态,此时可直接进行读写操作,其它缓存读取该缓存行需要通过总线想该核心发送请求进行读取,此时缓存行状态变为共享。核心可以读取缓存行数据,但修改需要先通过总线发起修改请求,将其它核心的该缓存行数据置为无效,再修改缓存行中数据(看起来效率很低?其实cpu会马上修改该缓存行,修改完后缓存不会马上生效,而是放入store buffer中,再反送失效请求给所有核心,由store buffer等待全部核心响应成功,再生效缓存行的修改,这段期间核心可以继续做其它事情)。修改完成后,缓存行为修改状态,其他核心的该缓存行为无效状态,此时若有核心读取该缓存行,就将缓存行写入内存,该核心上的缓存行状态更新为共享;若是修改请求,就将核心缓存行状态更新为无效。

  3. 原子指令:现代的多核处理器通常会提供一些原子指令,例如比较并交换(compare and swap)、加载和存储条件(load-linked/store-conditional)等。这些指令能够以硬件级别的原子方式执行,从而保证对共享数据的访问是原子的。通过使用这些原子指令,可以避免锁的开销,并提供更高效的原子操作。

2、atomic包

Golang的atomic包的原子操作是通过CPU指令实现的。在大多数CPU架构中,原子操作的实现都是基于32位或64位的寄存器。Golang的atomic包的原子操作函数会将变量的地址转换为指针型的变量,并使用CPU指令对这个指针型的变量进行操作。
Golang的atomic包提供了一组原子操作函数,包括Add、CompareAndSwap、Load、Store、Swap等函数。这些函数的具体作用如下:

  • Add函数:用于对一个整数型的变量进行加法操作,并返回新的值。
  • CompareAndSwap函数:用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。
  • Load函数:用于获取一个指针型的变量的值。
  • Store函数:用于设置一个指针型的变量的值。
  • Swap函数:用于交换一个指针型的变量的值,并返回旧值。
    让我们更具体地来看一下Golang的atomic包的原子操作:

2.1、 Add函数

Add函数用于对一个整数型的变量进行加法操作,并返回新的值。Add函数的定义如下:

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)

其中,addr表示要进行加法操作的变量的地址,delta表示要加上的值。Add函数会将变量的值加上delta,并返回新的值。

2.2、CompareAndSwap函数

CompareAndSwap函数用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。CompareAndSwap函数的定义如下:

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)

其中,addr表示要进行比较和交换的变量的地址,old表示旧值,new表示新值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。

2.3、Swap函数

Swap函数用于交换一个指针型的变量的值,并返回旧值。Swap函数的定义如下:

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)

其中,addr表示要交换的变量的地址,new表示新值。Swap函数会将变量的值设置为new,并返回旧值。

2.4、Load函数

Load函数用于获取一个指针型的变量的值。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)

2.5、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 *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

其中,addr表示要设置的变量的地址,val表示要设置的值。Store函数会将变量的值设置为val。

3、atomic.Value值

可以用来存储any类型对象的结构体,存取操作均是原子操作,具体方法如下:

type Value struct {
	v any
}
// 加载Value中存的值
func (v *Value) Load() (val any) {}
// 存储对象放入Value中
func (v *Value) Store(val any) {}
// 交换Value中存的数据
func (v *Value) Swap(new any) (old any) {)
// 比较并存储(值必须铜类型且可比较采用使用该方法,否则panic);传入old、new对象,如果old对象等于Value内存储的对象,就将新的对象存入,返回treu。否则false
func (v *Value) CompareAndSwap(old, new any) (swapped bool) {}

实践一下:

package main

import (
	"fmt"
	"sync/atomic"
)

type Config struct {
	Addr string
}

var config atomic.Value

func main() {
	conf1 := Config{
		Addr: "1.1.1.1",
	}
	conf2 := Config{
		Addr: "2.2.2.2",
	}
	config.Store(conf1)
	oldData := config.Swap(conf2)
	newData := config.Load()
	fmt.Println(oldData, newData)
	conf3 := Config{
		Addr: "3.3.3.3",
	}
	ok := config.CompareAndSwap(conf1, conf3)
	fmt.Println(ok, config.Load())
	ok = config.CompareAndSwap(conf2, conf3)
	fmt.Println(ok, config.Load())
}

输出:

{1.1.1.1} {2.2.2.2}
false {2.2.2.2}
true {3.3.3.3}

你可能感兴趣的:(golang,开发语言,后端)