• 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
    }
    
  • 相关阅读:
    jmeter监听每秒点击数(Hits per Second)
    哈希表原理、底层实现剖析
    30个必会python技巧
    C++套接字库sockpp介绍
    如何像人类一样写HTML之图像标签,超链接标签与多媒体标签
    第1章 Linux基础知识 -- 了解Linux历史和linux目录结构
    SpringBoot启动流程源码分析
    Kotlin 编程语言详解:特点、应用领域及语法教程
    iRDMA Flow Control Introduction
    微软用它取代了`Nginx`吞吐量提升了百分之八十!
  • 原文地址:https://blog.csdn.net/m0_62969222/article/details/127125640