Go语言并发编程——原子操作

一、原子操作

一个高并发的go程序在执行过程中,同一时刻只会有很少的Goroutine处于运行状态。Go语言的任务调度器为了公平起见,Goroutine会频繁的被换上和换下,它们不断的来回切换,从而达到并发的效果。

所以,一个Goroutine在执行某一个操作时很有可能会被中断,这就是非原子操作,也是并发不安全产生的原因。

原子操作就是在执行过程中是不会被中断的。在底层,这会由 CPU 提供芯片级别的支持,所以绝对有效。即使在拥有多 CPU 核心,或者多 CPU 的计算机系统中,原子操作的保证也是不可撼动的。

二、atomic包

sync/atomic包中的函数可以做的原子操作有:加法(add)、比较并交换(compare and swap,简称 CAS)、加载(load)、存储(store)和交换(swap)。

这些函数针对的数据类型并不多。但是,对这些类型中的每一个,sync/atomic包都会有一套函数给予支持。这些数据类型有:int32int64uint32uint64uintptr,以及unsafe包中的Pointer。不过,针对unsafe.Pointer类型,该包并未提供进行原子加法操作的函数。

add

func AddInt32(addr *int32, delta int32) (new int32)为例

  • addr *int32 为被加数的地址

  • delta int32 加数,当为负数时相当于减法

var i int32 = 10
i = atomic.AddInt32(&i,1)

load

load相当于原子性的读数据操作。在非原子读时,可能会造成:读到一半Goroutine被中断,当Goroutine再次被调度时,数据已被修改,那么最终读出来的就是一个奇怪的值。

func LoadInt32(addr *int32) (val int32)为例

  • addr *int32被读数据地址

  • val int32将读到的值返回

atomic.LoadInt32(&i)

store

store相当于原子写操作。同样的,在未使用原子写时,可能写到一半的数据被读到,所以要保证并发的安全性,要同时保证读和写的原子性,或者使用互斥锁保证读和写之间互斥,写和写之间也互斥。

func StoreInt32(addr *int32, val int32)为例

atomic.StoreInt32(&i,12)
  • addr *int32 要写入的内存地址

  • val int32 写入的值

swap

swap可以保证原子性的情况下交换两个数的值。

func SwapInt32(addr *int32, new int32) (old int32)为例

oldValue := atomic.SwapInt32(&i, 2)
  • addr *int32 进行交换的变量的地址

  • new int32 与变量交换的值

  • old int32 交换成功返回旧值

compare and swap(CAS)

swap是直接进行交换,而CAS是先进行比较,当条件满足时再进行交换。

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)为例

  • addr *int32,进行交换变量的地址

  • old, new int32 ,old为与变量比较的值(即交换条件),new是进行交换的值

    只有当前变量的值等于old时,才会将new和变量进行交换。

  • swapped bool,布尔类型返回值,满足条件并进行交换返回true,不满足条件直接返回false。

swapped := atomic.CompareAndSwapInt32(&i,0,1)

CAS实现自旋锁

我们可以借助CAS实现一个简单的自旋锁

package main

import (
	"runtime"
	"sync/atomic"
)

type spinlock int32

func (sl *spinlock) Lock() {
	for !atomic.CompareAndSwapInt32((*int32)(sl), 0, 1) {
		runtime.Gosched() //让出CPU时间片
		continue
	}
}

func (sl *spinlock) Unlock() {
	if atomic.LoadInt32((*int32)(sl)) == 0 {
		panic("error,unlock a unlocked lock")
	} else {
		atomic.StoreInt32((*int32)(sl), 0)
	}
}

三、原子操作与互斥锁对比

原子操作实现的功能我们使用互斥锁也能实现,但是原子操作是更加轻量级的。

原子操作会直接通过CPU指令保证当前Goroutine在执行操作时不会被其它线程所抢占。而互斥锁实现的操作,当前执行Goroutine是会被其它Goroutine抢占的,但是其它的Goroutine在未获取锁的情况并不能顺利执行,从而保证了并发的安全性。

所以,原子操作相对于互斥锁,大大的减少了同步Goroutine对程序性能的损耗。

原子操作能够使用的场景很少,是有很大局限性的。但是在能够使用原子操作的情况下,用它来代替互斥锁,对程序性能的提升是非常大的。

四、原子类型——Value

atomic包下提供的载入和读取操作可以让我们很容易的对基本数据进行读和写的原子操作,但是如果想要对其它类型进行原子性的读写操作。就需要我们借助unsafe包下的Pointer类型,开发者使用起来还是很麻烦的,于是就有了Value类型。

Value类型相当于一个容器,它可以并发安全的写入和读出任何类型,如下:

//自定义配置类型
type config struct {
    config1 string
    config2 string
}


var v atomic.Value
var config config
v.Store(&config)//载入
v.Load().(*config)//载出,需要进行类型转换

原子类型使用的注意事项:

  1. 不能用原子值存储nil

  2. 我们向原子值存储的第一个值,决定了它今后能且只能存储哪一个类型的值。

上面罗列了两条注意事项,但是为什么呢?我们从源码角度进行分析。

Value的源码分析

Value是一个结构体类型,结构体中只有一个空接口类型的字段。

type any = interface{}

type Value struct {
    v any
}

源码还用到了ifaceWords类型

type ifaceWords struct {
    typ  unsafe.Pointer
    data unsafe.Pointer
}

ifaceWords类型是空接口类型的内部表示格式,它可以将空接口类型接受到的数据分解为:type和data两部分。

与Value绑定的方法主要有两个:

  • v.Store(c) - 写操作,将原始的变量c存放到一个atomic.Value类型的v里。

  • c = v.Load() - 读操作,从线程安全的v中读取上一步存放的内容。

Store() —— 写操作

func (v *Value) Store(val any) {
    //传入值为nil时引起恐慌
    if val == nil {
        panic("sync/atomic: store of nil value into Value")
    }
    //Value中存储的指针
    vp := (*ifaceWords)(unsafe.Pointer(v))
    //被写入值的指针
    vlp := (*ifaceWords)(unsafe.Pointer(&val))
    for {
        //Value中存储值的类型
        typ := LoadPointer(&vp.typ)
        //此时类型为空,说明Value是第一次被写入
        if typ == nil {
            //第一次写入,需要分别写入type和data,本次写入并不能保证原子性
            //为了顺序完成写入操作,需要禁止当前Goroutine被抢占
            runtime_procPin()
            //if typ == nil 到 runtime_procPin()两段代码之间Goroutine可能被抢占
            //当typ为nil时,说明Goroutine没有被抢占,typ会进行值的交换,相当于给typ一个特殊标记,表示正在进行第一次写入
            //当typ不为nil时,说明Goroutine被抢占,其它Goroutine正在进行写入,进入if内部
            if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(&firstStoreInProgress)) {
                //取消当前Goroutine的禁止被抢占,重新进行写入
                runtime_procUnpin()
                continue
            }
            // 进行第一次写入
            //先写入值,再写入类型
            //这里不能先写入类型再写入值,因为写入类型时会将标记覆盖。看后面代码就会知道,其它Goroutine进行写入时会对标记进行判断
            StorePointer(&vp.data, vlp.data)
            StorePointer(&vp.typ, vlp.typ)
            //第一次写入完成,取消当前Goroutine的禁止被抢占
            runtime_procUnpin()
            return
        }

        //程序来到这里说明不是第一次被写入
        //判段typ上是否有标记,如果有标记,说明正在被其它当前Goroutine进行第一次写入
        //这里只需要考虑第一次被写入的情况,因为除第一次外,其它写入都是原子操作
        if typ == unsafe.Pointer(&firstStoreInProgress) {
            continue
        }
        // 写入值的类型和之前存储值的类型必须一致
        if typ != vlp.typ {
            panic("sync/atomic: store of inconsistently typed value into Value")
        }
        //写入数据,该步骤为原子操作
        StorePointer(&vp.data, vlp.data)
        return
    }
}

Load()——读操作

func (v *Value) Load() (val any) {
    //当前存储的值
    vp := (*ifaceWords)(unsafe.Pointer(v))
    //使用原子读操作,读取存储值的类型
    typ := LoadPointer(&vp.typ)
    //typ为nil表示Value还没有被写入值,typ上有标记,说明正在进行第一次写入
    if typ == nil || typ == unsafe.Pointer(&firstStoreInProgress) {
        // 直接返回nil,表示未读到
        return nil
    }
    //原子读操作
    data := LoadPointer(&vp.data)
    vlp := (*ifaceWords)(unsafe.Pointer(&val))
    //赋值type和data给返回值
    vlp.typ = typ
    vlp.data = data
    return
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值