Go并发原子操作 waitGroup 对象池

 更新时间:2026年04月08日 09:22:00   作者:念何架构之路  
本文详细介绍了Go语言中原子操作和并发同步工具的使用,文章通过代码示例详细说明了这些并发工具的正确使用方式,并分析了它们的实现原理和性能特点,下面就来详细的介绍一下

原子操作:

原子操作即执行过程中不可被中断的操作.在针对某个值的原子操作执行过程当中.CPU绝不会再去执行其它针对该值的操作.无论这些操作是否为原子操作.

Go语言提供的原子操作都是非侵入式的.它们由标准库代码包sync/atomic中的众多函数代表.可以通过调用这些函数对几种简单类型执行原子操作.

增或减:

用于增或减的原子操作(以下简称原子增或减操作)的函数名称都以Add为前缀.后跟针对具体类型的名称.

示例:

func main() {
    var a int32 = 1
    var b int64 = 1
    int32Value := atomic.AddInt32(&a, 1)
    int64Value := atomic.AddInt64(&b, 1)
    fmt.Println(int32Value)
    fmt.Println(int64Value)
    reduceInt32 := atomic.AddInt32(&a, -1)
    reduceInt64 := atomic.AddInt64(&b, -1)
    fmt.Println(reduceInt32)
    fmt.Println(reduceInt64)
}

执行结果:

比较并交换:

比较并交换即"Compare And Swap"简称CAS.在sync/atomic包中.这类原子操作由名称以"CompareAndSwap"为前缀的若干函数代表.

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

函数接受三个参数.第一个参数的值是指向被操作的指针值.该值的类型是*int32.后两个参数的类型也是32.分别代表被操作的旧值和新值.函数在被调用后.会先判断参数addr的指向和旧值old的值是否相等.当判断成功后.才会用新值替换旧值.否则.后面的替换就会被忽略.

与前面的锁相比.CAS操作有明显不同.它总是假设被操作值未曾改变(即与旧值相等).并一旦确认这个假设的真实性就立即进行值替换.使用锁则是更加谨慎的做法.总是先假设会有并发操作会修改被操作值.并需要使用锁将相关操作放入临界区加以保护.可以说.使用锁的做法趋于悲观.而CAS操作的做法比较乐观.

CAS操作的优势是.可以在不创建互斥量和不形成临界区的情况下.完成并发安全的值替换操作.可以大大减少同步对程序性能的损耗.当然,CAS也有劣势.是被操作值被频繁变更的情况下.CAS操作并不那么容易成功.有时候.可能不得不利用for循环来进行多次尝试.

示例:

func main() {
    var value int32
    for i := 0; i < 10; i++ {
       go func() {
          for {
             v := value
             if atomic.CompareAndSwapInt32(&v, 0, 1) {
                break
             }
          }
       }()
    }
}

从上面例子可以看出.只有保证CAS操作成功后.for循环才会结束退出循环.CAS操作虽然不会阻塞goroutine.但是会不断自旋造成性能上的消耗.

载入:

前面展示的for循环中.使用语句v:=value为变量v赋值.要注意.在读取value的过程中.并不能保证没有对此值的并发读写操作.为了原子的读取某个值.sync/atomic代码包也提供了一系列函数.这些函数的名称都以"load"(意为载入)为前缀.

示例:

func main() {
    var value int32
    for i := 0; i < 10; i++ {
       go func() {
          for {
             v := atomic.LoadInt32(&value)
             if atomic.CompareAndSwapInt32(&v, 0, 1) {
                break
             }
          }
       }()
    }
}

函数atomic.LoadInt32接受一个*int32类型的指针值.并会返回该指针值指向的那个值.

注意:虽然这里使用了atomic.LoadInt32函数原子的载入value的值.后面的CAS操作仍然是有必要的.因为.赋值语句后面的if语句并不会原子执行.在它们执行期间.CPU仍然可能执行其它针对Value的操作.

存储:

与读操作相对应的是写入操作.而sync/atomic包也提供了对应的存储函数.这些函数的名称均以"Store"为前缀.

在原子的存储某个值的过程中.任何CPU都不会进行针对同一个值的读写操作.如果把所有针对此值的写操作都改为原子操作.就绝不会出现针对此值的读操作因被并发的进行.而读到修改了一半的值的情况.

原子的值存储操作总会成功.因为它并不关系操作的旧值是什么.这与前面的CAS操作有明显区别.

函数atomic.StoreInt32会接受两个参数.第一个参数的类型是*int32.它同样是指向被操作值的指针值.第二个参数是int32类型.它的值是欲存储的新值.

交换:

在sync/atomic代码包中还有一些函数.它们的功能与前文所讲的CAS操作和原子载入操作都有相似之处.这里的功能可以称为原子交换操作.这类函数的名称都以"Swap"为前缀.

与CAS操作不同.原子操作不会关心被操作的旧值.而是直接设置新值.但它又比原子存储操作多做了一步.它会返回被操作的旧值.此类操作比CAS操作的约束更少.同时又比原子载入操作的功能更强.

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

从函数可知.它接受两个参数.其中第一个参数代表了被操作的内存地址的*int32类型值.第二个参数用来表示新值.注意.该函数是有结果值的.该值即被新值替换掉的旧值.函数被调用后.会把第二个参数的值置于第一个参数值所代表的的内存地址上.并将之前在该地址上的那个值作为结果返回.

原子值:

sync/atomic.Value是一个结构体类型.暂且称为原子类型.它用于存储需要原子读写的值.与sync/atomic包中的其它函数不同.sync/atomic.Value可接受的被操作值的类型不限.

该类型有两个公开的指针方法Load和Store.前者用于原子的读取原子值实例中存储的值.它会返回一个interface{}类型的结果且不接受任何参数.后者用于原子的在原子实例中存储一个值.它接受一个interface{}类型的参数而没有任何结果.在未曾通过Store方法向原子值实例存储值之前.它的Load方法总会返回nil.

对于原子值实例的Store方法有两个限制.第一.作为参数传入该方法的值不能为nil.第二.作为参数传入该方法的值必须与之前传入的值(如果有的话)的类型相同.一旦原子值实例存储了某一个类型的值.那么它之后存储的值就必须是该类型的.如果违反了任意一个限制.对该方法的调用都会引发一个运行是恐慌.

严格来说.sync/atomic.Value类型的变量一旦声明.其值就不应该被复制到它处.作为源值赋给别的变量 作为参数值传入参数 作为结果值从函数返回 作为元素值通过通道传递等都会造成值的复制.所以这类变量之上不应该实施这些操作.虽然编译不会错误.但Go标准工具go vet会报告此类不正确(或说有安全隐患).不过原子类型不会有这个问题.根本原因.对结构体值的复制不但会生成该值的副本.还会生成其中字段的副本.这样一来.本应施加于此的并发安全也就失效了.向副本存储值的操作也与原值无关了.

示例:

func main() {
    var countVal atomic.Value
    countVal.Store([]int{1, 3, 5, 7})
    anotherStore(countVal)
    fmt.Printf("The count value: %v\n", countVal.Load())
}

func anotherStore(countVal atomic.Value) {
    countVal.Store([]int{2, 4, 6, 8})
}

执行结果:

使用场景:

type ConcurrentArray interface {
    //设置指定索引上的元素值.
    Set(index uint32,elem int) (err error)
    //用于获取指定索引元素的值.
    Get(index uint32) (elem int, err error)
    //用于获取元素的长度.
    Len() uint32
}

接口实现:

type concurrentArray struct {
    length uint32
    val atomic.Value
}

创建函数:

func NewConcurrentArray(length uint32) ConcurrentArray {
    array := concurrentArray{}
    array.length = length
    array.val.Store(make([]int, array.length))
    return &array
}

接口实现方法:

func (array *concurrentArray) Len() uint32 {
    return array.length
}

func (array *concurrentArray) Set(index uint32, elem int) (err error) {
    newArray := make([]int, array.length)
    copy(newArray, array.val.Load().([]int))
    newArray[index] = elem
    array.val.Store(newArray)
    return
}

func (array *concurrentArray) Get(index uint32) (elem int, err error) {
    elem = array.val.Load().([]int)[index]
    return elem,nil
}

只执行一次:

与互斥锁和读写锁一样,sync.Once也是开箱即用.

var once sync.Once

once.Do(func() {fmt.Println("Once")})

声明了一个名为once的sync.Once类型的变量.然后用执行它的Do方法.Do方法接受一个无参数 无结果的函数值作为参数.该方法一旦被调用.就会调用作为参数的那个函数.对同一个sync.Once类型值的Do方法的有效调用次数永远是一次.

示例:

func main() {
    var count int
    var once sync.Once
    max := rand.Intn(100)
    for i := 0; i < max; i++ {
       once.Do(func() {
          count++
       })
    }
    fmt.Printf("Count: %d\n", count)
}

waitGroup:

sync.WAitGroup类型的值是并发安全的.也是开箱即用的.在声明中var wg sync.WaitGroup之后.就可以直接使用wg变量了.该类型有三个方法.即Add Done和wait.

func main() {
    var wg sync.WaitGroup
    wg.Add(3)
    for i := 0; i < 3; i++ {
       go func() {
          fmt.Printf("协程:%d执行了.\n", i)
          time.Sleep(1 * time.Second)
          wg.Done()
       }()
    }
    fmt.Println("等待协程开始执行")
    wg.Wait()
    fmt.Println("所有协程都执行完了.结束主线程")
}

使用规则:

1).对一个sync.WaitGroup类型值的Add方法的第一次调用.发生在调用该值的Done之前.

2).对一个sync.WaitGroup类型值的Add方法的第一次调用.同样发生在调用该值的wait方法之前.

3).在一个sync.WaitGroup类型值的生命周期内.其中的给定计数总是由起初的0变为某个正整数(或先后变为某几个正整数).然后在回归为0.完成这样的变化曲线所用的时间称为一个计数周期.

4).给定计数的每次变化都是由对Add方法或Done方法的调用引起的.一个计数的周期总是从Add方法调用开始的.并且也总是以对Add方法或Done方法的调用为结束标志.在一个计数周期之内调用wait方法.就会使调用所在的goroutine阻塞.直到该计数周期结束的那一刻.

5).sync.WaitGroup类型值是可以复用的.此类值的生命周期可以包含任意个计数周期.一旦一个计数周期结束.在前面对该值的方法调用所产生的作用就会消失.它们不会影响该值的后续计数周期.一个sync.WaitGroup类型值在其每个计数周期中的状态和作用都是独立的.

临时对象池:

sync.Pool类型值可以看作存放临时值的容器.此类容器是自动伸缩的 高效的.同时也是并发安全的.为了描述方便.把sync.Pool类型的值称为"临时对象池".而把存于其中的值称为"对象值".

在用复合字面量初始化一个临时对象池的时候.可以为它唯一的公开字段New赋值.该字段的类型是func() interface{}.即一个函数类型.赋给该字段的函数会被临时对象池用来创建对象值.不过.该函数一般仅在池中无可用对象值的时候才被调用..把这个函数称为"对象值生成函数".

sync.Pool类型由两个公开的指针方法:Get和Put.前者的功能是从池中获取一个interface{}类型的值.后者的作用则是把一个interface{}类型的值放置于池中.

通过Get方法获取到的值是任意的.如果一个临时对象池的Put方法从未被调用过.且它的New字段也未曾被赋予一个非null的函数值.那么它的Get方法返回的结果就一定会是nil.临时对象池在功能上与一个通用的缓存池有一些相似.实际上.临时对象池本身的特性决定了它是一个很独特的同步工具.

特性:

1).临时对象池可以把由其中的对象值产生的存储压力进行分摊.更进一步说.它会专门为每一个与操作它的goroutine相关联的P建立本地池.在临时对象池的Get方法被调用时.它一般会先尝试从与本地P对应的那个本地私有池和本地共享池中获取一个对象池.如果获取失败.它就会尝试从其它P的本地共享池中偷一个对象值并直接返回给调用方.如果依然未果.它就只能把希望寄托于当前临时对象池的对象生成函数了.注意.这个对象值生成函数生成的对象永远不会被放置到池中.而是会被直接返回给调用方.另一方面.临时对象池的Put方法会把它的参数值存放到本地P的本地池中.每个相关的P的本地共享池中所有对象值.都是在当前临时对象池的范围内共享的.也就是说.它们随时可能被偷走.

2).对垃圾回收友好.垃圾回收的执行一般会使临时对象池中的对象值全部移除.也就是说.即使永远不会显示的从临时对象池中取走某个对象值.该对象值也不会永远待在临时对象池中.它的生命周期取决于垃圾回收任务的下一次执行时间.

func main() {
    // 1. 保存原始GC配置,函数结束后恢复(正确写法)
    originalGC := debug.SetGCPercent(-1)
    defer debug.SetGCPercent(originalGC)

    var count int32
    // 统一类型:全部存储 int32 类型
    newFunc := func() interface{} {
       return atomic.AddInt32(&count, 1)
    }
    pool := sync.Pool{New: newFunc}

    // 池空,调用New函数
    v1 := pool.Get()
    fmt.Printf("Value 1: %v\n", v1)

    // 存入与New函数返回值 相同类型 的对象(int32)
    pool.Put(int32(10))
    pool.Put(int32(11))
    pool.Put(int32(12))
    v2 := pool.Get()
    fmt.Printf("Value 2: %v\n", v2)

    // 手动触发GC,Pool缓存会被回收
    debug.SetGCPercent(100)
    runtime.GC()

    // GC后缓存清空,再次调用New
    v3 := pool.Get()
    fmt.Printf("Value 3: %v\n", v3)

    // 清空New函数
    pool.New = nil
    v4 := pool.Get()
    fmt.Printf("Value 4: %v\n", v4)
}

到此这篇关于Go并发原子操作 waitGroup 对象池的文章就介绍到这了,更多相关Go原子操作waitGroup 对象池内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go语言中防止敏感数据意外泄露的几种方法

    Go语言中防止敏感数据意外泄露的几种方法

    这篇文章主要介绍了在Go语言中优雅地处理敏感数据的方法,通过使用接口来控制格式化、日志记录、序列化等行为,避免敏感信息泄露,需要的朋友可以参考下
    2026-01-01
  • Golang利用compress/flate包来压缩和解压数据

    Golang利用compress/flate包来压缩和解压数据

    在处理需要高效存储和快速传输的数据时,数据压缩成为了一项不可或缺的技术,Go语言的compress/flate包为我们提供了对DEFLATE压缩格式的原生支持,本文将深入探讨compress/flate包的使用方法,揭示如何利用它来压缩和解压数据,并提供实际的代码示例,需要的朋友可以参考下
    2024-08-08
  • Go调用链可视化工具使用实例探究

    Go调用链可视化工具使用实例探究

    本文介绍一款工具 go-callvis,它能够将 Go 代码的调用关系可视化出来,并提供了可交互式的 web 服务,在接手他人代码或调研一些开源项目时,如果能够理清其中的代码调用链路,这将加速我们对实现的理解
    2024-01-01
  • Go语言学习之context包的用法详解

    Go语言学习之context包的用法详解

    日常Go开发中,Context包是用的最多的一个了,几乎所有函数的第一个参数都是ctx,那么我们为什么要传递Context呢,Context又有哪些用法,底层实现是如何呢?相信你也一定会有探索的欲望,那么就跟着本篇文章,一起来学习吧
    2022-10-10
  • 使用Go goroutine实现并发的Clock服务

    使用Go goroutine实现并发的Clock服务

    这篇文章主要为大家详细介绍了如何使用Go goroutine实现并发的Clock服务,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-06-06
  • golang int 转float 强转和高精度转操作

    golang int 转float 强转和高精度转操作

    这篇文章主要介绍了golang int 转float 强转和高精度转操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go语言实现牛顿法求平方根函数的案例

    Go语言实现牛顿法求平方根函数的案例

    这篇文章主要介绍了Go语言实现牛顿法求平方根函数的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • golang并发下载多个文件的方法

    golang并发下载多个文件的方法

    今天小编就为大家分享一篇golang并发下载多个文件的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-07-07
  • 深入解析golang编程中函数的用法

    深入解析golang编程中函数的用法

    这篇文章主要介绍了golang编程中函数的用法,是Go语言入门学习中的基础知识,需要的朋友可以参考下
    2015-10-10
  • 使用Go中的Web3库进行区块链开发的案例

    使用Go中的Web3库进行区块链开发的案例

    区块链作为一种分布式账本技术,在近年来取得了巨大的发展,而Golang作为一种高效、并发性强的编程语言,被广泛用于区块链开发中,本文将介绍如何使用Golang中的Web3库进行区块链开发,并提供一些实际案例,需要的朋友可以参考下
    2023-10-10

最新评论