[go]原子操作atomic包简介


原子操作就是不可中断的操作,这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界是看不到原子操作的中间状态。

简介

go语言通过内置sync/atomic包提供对原子操作的支持,包括(以下XXType为:int32int64uint32uint64uintptr):

  • 增减操作(AddXXType):保证对操作数的原子增减;
  • 载入操作(LoadXXType):保证读取到操作数前,没有其他routine对其进行更改操作;
  • 存储操作(StoreXXType):保证存储时的原子性(避免被其他线程读取到修改一半的数据);
  • 比较并交互操作(CompareAndSwapXXType):保证交换的CAS,只有原有值没被更改时才会交换;
  • 交换操作(SwapXXType):直接交换,不关心原有值。

原子操作与互斥锁区别

sync包中的同步的原语Mutex常用于保证并发安全,其与atomic有什么区别呢:

  • 目的不同:Mutex用于保护一段逻辑操作,atomic用于保护对变量的更新;
  • 底层实现不同:
    • Mutex由操作系统的调度器实现;
    • atomic中原子操作由底层硬件指令直接提供支持;指令在执行的过程中是不允许中断的,因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多而线性扩展。

对于一个变量更新,原子操作通常会更有效率,并且更能利用计算机多核的优势。

原子操作

所有原子操作方法的被操作数形参必须是指针类型,通过指针变量可以获取被操作数在内存中的地址,从而施加特殊的CPU指令,确保同一时间只有一个goroutine能够进行操作。

增加操作

所有增减操作以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)

原子减操作:

   var opts int64 = 100

   for i := 0; i < 50; i++ { 
       atomic.AddInt64(&opts, -1) // 减操作
       time.Sleep(time.Millisecond)
   }
Uint64减操作

对于有符号整数,直接Add对应的负数即可实现减操作;但是对于无符号整数,则会报错。

若要从无符号整数中减去c,则需要通过Add其对应负数的补码实现:即AddUint64(&x, ^uint64(c-1))
所以要减一就是:AddUint64(&x, ^uint64(0))

比较并交换操作

比较并交换操作以CompareAndSwap为前缀:

  • func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
  • func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
  • func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (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)

CAS操作类似常见的乐观锁机制;该操作在进行交换前首先确保变量的值未被更改,即仍然保持参数 old 所记录的值,满足此前提下才进行交换操作。当有大量的goroutine 对变量进行读写操作时,可能导致CAS操作无法成功,这时可以利用for循环多次尝试。

func atomicAddOp(tmp int64) {
for {
       oldValue := value
       if atomic.CompareAndSwapInt64(&value, oldValue, oldValue+tmp) {
           return
       }
   }
}

载入操作

载入操作都以Load为前缀(可避免读取到写入一半的数据):

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

atomic.Value

sync/atomic包中的类型Value(相当于一个容器),可用来**“原子地”**存储或加载任意类型的值。

  • func(v *Value) Load() (x interface{}): 读操作,从线程安全的v中读取上一步存放的内容
  • func(v *Value) Store(x interface{}): 写操作,将原始的变量x存放在atomic.Value类型中;

value存储的类型要求:

  • 不能存储nil(存nil会抛出panic);
  • value中存储的第一个值,决定了其后续的值类型(以后只能存储此类型的值);
  • 尝试存储不同的类型,会抛出panic;
import (
  "sync/atomic"
  "time"
)

func loadConfig() map[string]string {
  // 从数据库或者文件系统中读取配置信息,然后以map的形式存放在内存里
  return make(map[string]string)
}

func requests() chan int {
  // 将从外界中接收到的请求放入到channel里
  return make(chan int)
}

func main() {
  // config变量用来存放该服务的配置信息
  var config atomic.Value
  // 初始化时从别的地方加载配置文件,并存到config变量里
  config.Store(loadConfig())
  go func() {
    // 每10秒钟定时的拉取最新的配置信息,并且更新到config变量里
    for {
      time.Sleep(10 * time.Second)
      // 对应于赋值操作 config = loadConfig()
      config.Store(loadConfig())
    }
  }()
  // 创建工作线程,每个工作线程都会根据它所读取到的最新的配置信息来处理请求
  for i := 0; i < 10; i++ {
    go func() {
      for r := range requests() {
        // 对应于取值操作 c := config
        // 由于Load()返回的是一个interface{}类型,所以我们要先强制转换一下
        c := config.Load().(map[string]string)
        // 这里是根据配置信息处理请求的逻辑...
        _, _ = r, c
      }
    }()
  }
}

value读取

若value中没有存储过值(typ是nil;或^uintptr(0),正在存储值),则直接返回nil;否则根据存储的typ和data构造出新的interface{}。

func (v *Value) Load() (x interface{}) {
    // 将*Value指针类型转换为*ifaceWords指针类型
	vp := (*ifaceWords)(unsafe.Pointer(v))
	// 原子性的获取到v的类型typ的指针
	typ := LoadPointer(&vp.typ)
	// 如果没有写入或者正在写入,先返回,^uintptr(0)代表过渡状态
	if typ == nil || uintptr(typ) == ^uintptr(0) {
		return nil
	}
	// 原子性的获取到v的真正的值data的指针,然后返回
	data := LoadPointer(&vp.data)
	xp := (*ifaceWords)(unsafe.Pointer(&x))
	xp.typ = typ
	xp.data = data
	return
}

ifaceWords类型,作为空interface的内部表示格式

  • typ代表原始类型
  • data代表真正的值
// ifaceWords is interface{} internal representation.
type ifaceWords struct {
	typ  unsafe.Pointer
	data unsafe.Pointer
}

value存储

存储的值不能是nil;后续存储的类型,必须与首次存储时的类型相同。首次存储时通过runtime_procPin锁定(当前goroutine,对当前协程绑定的线程加锁;禁止其他goroutine的抢占);完成后通过runtime_procUnpin释放。

func (v *Value) Store(x interface{}) {
	if x == nil {
		panic("sync/atomic: store of nil value into Value")
	}
	// 将现有的值和要写入的值转换为ifaceWords类型(以便获取到它们的原始类型和真正的值)
	vp := (*ifaceWords)(unsafe.Pointer(v))
	xp := (*ifaceWords)(unsafe.Pointer(&x))
	for {
		// 获取现有的值的type
		typ := LoadPointer(&vp.typ)
		// 如果typ为nil说明这是第一次Store
		if typ == nil {
			// 第一次,就锁定当前的processor,禁止其他goroutine抢占
			runtime_procPin()
			// 使用CAS操作,先尝试将typ设置为^uintptr(0)这个中间状态
			// 如果失败,则证明已经有别的线程抢先完成了赋值操作
			if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
				runtime_procUnpin()
				continue
			}
			// 如果设置成功,就原子性的更新对应的指针,最后解除抢占锁
			StorePointer(&vp.data, xp.data)
			StorePointer(&vp.typ, xp.typ)
			runtime_procUnpin()
			return
		}
		// typ为^uintptr(0)说明第一次写入还没有完成,继续循环等待
		if uintptr(typ) == ^uintptr(0) {
			continue
		}
		// 写入的类型和现有的类型不一致,则panic
		if typ != xp.typ {
			panic("sync/atomic: store of inconsistently typed value into Value")
		}
		// 更新data
		StorePointer(&vp.data, xp.data)
		return
	}
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值