脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - Golang 五种原子性操作的用法详解

Golang 五种原子性操作的用法详解

2021-09-27 00:53网管叨bi叨KevinYan11 Golang

本文我们详细聊一下 Go 语言的原子操作的用法,啥是原子操作呢?顾名思义,原子操作就是具备原子性的操作...

 Golang 五种原子性操作的用法详解

本文我们详细聊一下 Go 语言的原子操作的用法,啥是原子操作呢?顾名思义,原子操作就是具备原子性的操作... 是不是感觉说了跟没说一样,原子性的解释如下:

一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为 原子性(atomicity) 。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。

CPU 执行一系列操作时不可能不发生中断,但如果我们在执行多个操作时,能让他们的 中间状态对外不可见 ,那我们就可以宣称他们拥有了"不可分割”的原子性。

类似的解释我们在数据库事务的 ACID 概念里也听过。

Go 语言提供了哪些原子操作

Go 语言通过内置包 sync/atomic 提供了对原子操作的支持,其提供的原子操作有以下几大类:

  •  
    1. AddXXXType 
    2. int32 
    3. int64 
    4. uint32 
    5. uint64 
    6. uintptr 
    7. XXXType 
  • 载入,保证了读取到操作数前没有其他任务对它进行变更,操作方法的命名方式为 LoadXXXType ,支持的类型除了基础类型外还支持 Pointer ,也就是支持载入任何类型的指针。
  • 存储,有载入了就必然有存储操作,这类操作的方法名以 Store 开头,支持的类型跟载入操作支持的那些一样。
  •  
    1. CAS 
    2. Go 
    3. CAS 
  • 交换,这个简单粗暴一些,不比较直接交换,这个操作很少会用。

互斥锁跟原子操作的区别

平日里,在并发编程里,Go语言 sync 包里的同步原语 Mutex 是我们经常用来保证并发安全的,那么他跟 atomic 包里的这些操作有啥区别呢?在我看来他们在使用目的和底层实现上都不一样:

  • 使用目的:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。

  • 底层实现: Mutex 由 操作系统 的调度器实现,而 atomic 包中的原子操作则由 底层硬件指令 直接提供支持,这些指令在执行的过程中是不允许中断的,因此原子操作可以在 lock-free 的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展。

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

比如下面这个,使用互斥锁的并发计数器程序:

  1. func mutexAdd() { 
  2.  var a int32 =  0 
  3.  var wg sync.WaitGroup 
  4.  var mu sync.Mutex 
  5.  start := time.Now() 
  6.  for i := 0; i < 100000000; i++ { 
  7.   wg.Add(1
  8.   go func() { 
  9.    defer wg.Done() 
  10.    mu.Lock() 
  11.    a += 1 
  12.    mu.Unlock() 
  13.   }() 
  14.  } 
  15.  wg.Wait() 
  16.  timeSpends := time.Now().Sub(start).Nanoseconds() 
  17.  fmt.Printf("use mutex a is %d, spend time: %v\n", a, timeSpends) 

把 Mutex 改成用方法 atomic.AddInt32(&a, 1) 调用,在不加锁的情况下仍然能确保对变量递增的并发安全。

  1. func AtomicAdd() { 
  2.  var a int32 =  0 
  3.  var wg sync.WaitGroup 
  4.  start := time.Now() 
  5.  for i := 0; i < 1000000; i++ { 
  6.   wg.Add(1
  7.   go func() { 
  8.    defer wg.Done() 
  9.    atomic.AddInt32(&a, 1
  10.   }() 
  11.  } 
  12.  wg.Wait() 
  13.  timeSpends := time.Now().Sub(start).Nanoseconds() 
  14.  fmt.Printf("use atomic a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends) 

可以在本地运行以上这两段代码,可以观察到计数器的结果都最后都是 1000000 ,都是线程安全的。

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

上面的例子除了增加操作外我们还演示了载入操作,接下来我们来看一下 CAS 操作。

比较并交换

该操作简称 CAS (Compare And Swap)。这类操作的前缀为 CompareAndSwap :

  1. func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) 
  2.  
  3. func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) 

该操作在 进行交换前首先确保被操作数的值未被更改,即仍然保存着参数 old 所记录的值,满足此前提条件下才进行交换操作 。 CAS 的做法类似操作数据库时常见的乐观锁机制。

需要注意的是,当有大量的goroutine 对变量进行读写操作时,可能导致 CAS 操作无法成功,这时可以利用 for 循环多次尝试。

上面我只列出了比较典型的 int32 和 unsafe.Pointer 类型的 CAS 方法,主要是想说除了读数值类型进行比较交换,还支持对指针进行比较交换。

unsafe.Pointer提供了绕过Go语言指针类型限制的方法,unsafe指的并不是说不安全,而是说官方并不保证向后兼容。

  1. // 定义一个struct类型P 
  2. type P struct{ x, y, z int } 
  3.    
  4. // 执行类型P的指针 
  5. var pP *P 
  6.    
  7. func main() { 
  8.    
  9.     // 定义一个执行unsafe.Pointer值的指针变量 
  10.     var unsafe1 = (*unsafe.Pointer)(unsafe.Pointer(&pP)) 
  11.    
  12.     // Old pointer 
  13.     var sy P 
  14.    
  15.     // 为了演示效果先将unsafe1设置成Old Pointer 
  16.     px := atomic.SwapPointer( 
  17.         unsafe1, unsafe.Pointer(&sy)) 
  18.    
  19.     // 执行CAS操作,交换成功,结果返回true 
  20.     y := atomic.CompareAndSwapPointer( 
  21.         unsafe1, unsafe.Pointer(&sy), px) 
  22.    
  23.     fmt.Println(y) 

上面的示例并不是在并发环境下进行的 CAS ,只是为了演示效果,先把被操作数设置成了 Old Pointer 。

其实 Mutex 的底层实现也是依赖原子操作中的 CAS 实现的,原子操作的 atomic 包相当于是 sync 包里的那些同步原语的实现依赖。

比如互斥锁 Mutex 的结构里有一个 state 字段,其是表示锁状态的状态位。

  1. type Mutex struct { 
  2.  state int32 
  3.  sema  uint32 

为了方便理解,我们在这里将它的状态定义为0和1,0代表目前该锁空闲,1代表已被加锁,以下是 sync.Mutex 中 Lock 方法的部分实现代码。

  1. func (m *Mutex) Lock() { 
  2.    // Fast path: grab unlocked mutex. 
  3.    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { 
  4.        if race.Enabled { 
  5.            race.Acquire(unsafe.Pointer(m)) 
  6.        } 
  7.        return 
  8.    } 
  9.    // Slow path (outlined so that the fast path can be inlined) 
  10.     m.lockSlow() 

在 atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) 中, m.state 代表锁的状态,通过 CAS 方法,判断锁此时的状态是否空闲( m.state==0 ),是,则对其加锁( mutexLocked 常量的值为1)。

atomic.Value保证任意值的读写安全

atomic 包里提供了一套 Store 开头的方法,用来保证各种类型变量的并发写安全,避免其他操作读到了修改变量过程中的脏数据。

  1. func StoreInt32(addr *int32, val int32) 
  2.  
  3. func StoreInt64(addr *int64, val int64) 
  4.  
  5. func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) 
  6.  
  7. ... 

这些操作方法的定义与上面介绍的那些操作的方法类似,我就不再演示怎么使用这些方法了。

值得一提的是如果你想要并发安全的设置一个结构体的多个字段,除了把结构体转换为指针,通过 StorePointer 设置外,还可以使用 atomic 包后来引入的 atomic.Value ,它在底层为我们完成了从具体指针类型到 unsafe.Pointer 之间的转换。

有了 atomic.Value 后,它使得我们可以不依赖于不保证兼容性的 unsafe.Pointer 类型,同时又能将任意数据类型的读写操作封装成原子性操作(中间状态对外不可见)。

atomic.Value 类型对外暴露了两个方法:

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

1.17 版本我看还增加了 Swap 和 CompareAndSwap 方法。

简洁的接口使得它的使用也很简单,只需将需要做并发保护的变量读取和赋值操作用 Load() 和 Store() 代替就行了。

由于 Load() 返回的是一个 interface{} 类型,所以在使用前我们记得要先转换成具体类型的值,再使用。下面是一个简单的例子演示 atomic.Value 的用法。

  1. type Rectangle struct { 
  2.  length int 
  3.  width  int 
  4.  
  5. var rect atomic.Value 
  6.  
  7. func update(width, length int) { 
  8.  rectLocal := new(Rectangle) 
  9.  rectLocal.width = width 
  10.  rectLocal.length = length 
  11.  rect.Store(rectLocal) 
  12.  
  13. func main() { 
  14.  wg := sync.WaitGroup{} 
  15.  wg.Add(10
  16.  // 10 个协程并发更新 
  17.  for i := 0; i < 10; i++ { 
  18.   go func() { 
  19.    defer wg.Done() 
  20.    update(i, i+5
  21.   }() 
  22.  } 
  23.  wg.Wait() 
  24.  _r := rect.Load().(*Rectangle) 
  25.  fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length) 

你也可以试试,不用 atomic.Value ,直接给 Rectange 类型的指针变量赋值,看看在并发条件下,两个字段的值是不是能跟预期的一样变成10和15。

总结

本文详细介绍了Go语言原子操作 atomic 包中会被高频使用的操作的使用场景和用法,当然我并没有罗列 atomic 包里所有操作的用法,主要是考虑到有的用到的地方实在不多,或者是已经被更好的方式替代,还有就是觉得确实没必要,看完本文的内容相信你已经完全具备自行探索 atomic 包的能力了。

再强调一遍,原子操作由 底层硬件 支持,而锁则由操作系统的 调度器 实现。锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用 atomic.Value 封装好的实现。

给网管个星标第一时间吸我的知识 :point_up_2:

原文链接:https://mp.weixin.qq.com/s?__biz=MzUzNTY5MzU2MA==&mid=2247489229&idx=1&sn=3674ab103ec4dd704f0e9d1a784eeed4&utm_source=tuicool&utm_medium=referral

延伸 · 阅读

精彩推荐
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21