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

目录
  • Go 语言提供了哪些原子操作
  • 互斥锁跟原子操作的区别
  • 比较并交换
  • atomic.Value保证任意值的读写安全
  • 总结

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

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

CPU执行一系列操作时不可能不发生中断,但如果我们在执行多个操作时,能让他们的中间状态对外不可见,那我们就可以宣称他们拥有了"不可分割”的原子性。
类似的解释我们在数据库事务的ACID概念里也听过,只不过这里保障原子性的执行体是CPU。

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

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

  • 增减,操作方法的命名方式为AddXXXType,保证对操作数进行原子的增减,支持的类型为int32、int64、uint32、uint64、uintptr,使用时以实际类型替换前面我说的XXXType就是对应的操作方法。
  • 载入,保证了读取到操作数前没有其他任务对它进行变更,操作方法的命名方式为LoadXXXType,支持的类型除了基础类型外还支持Pointer,也就是支持载入任何类型的指针。
  • 存储,有载入了就必然有存储操作,这类操作的方法名以Store开头,支持的类型跟载入操作支持的那些一样。
  • 比较并交换,也就是CAS (Compare And Swap),像Go的很多并发原语实现就是依赖的CAS操作,同样是支持上面列的那些类型。
  • 交换,这个简单粗暴一些,不比较直接交换,这个操作很少会用。

互斥锁跟原子操作的区别

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

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

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

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

func mutexAdd() {
 var a int32 =  0
 var wg sync.WaitGroup
 var mu sync.Mutex
 start := time.Now()
 for i := 0; i < 100000000; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   mu.Lock()
   a += 1
   mu.Unlock()
  }()
 }
 wg.Wait()
 timeSpends := time.Now().Sub(start).Nanoseconds()
 fmt.Printf("use mutex a is %d, spend time: %v\n", a, timeSpends)
}

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

func AtomicAdd() {
 var a int32 =  0
 var wg sync.WaitGroup
 start := time.Now()
 for i := 0; i < 1000000; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   atomic.AddInt32(&a, 1)
  }()
 }
 wg.Wait()
 timeSpends := time.Now().Sub(start).Nanoseconds()
 fmt.Printf("use atomic a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends)
}

可以在本地运行以上这两段代码,可以观察到计数器的结果都最后都是1000000,都是线程安全的。
需要注意的是,所有原子操作方法的被操作数形参必须是指针类型,通过指针变量可以获取被操作数在内存中的地址,从而施加特殊的CPU指令,确保同一时间只有一个goroutine能够进行操作。
上面的例子除了增加操作外我们还演示了载入操作,接下来我们来看一下CAS操作。

比较并交换

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

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

func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

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

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

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

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

// 定义一个struct类型P
type P struct{ x, y, z int }

// 执行类型P的指针
var pP *P

func main() {

    // 定义一个执行unsafe.Pointer值的指针变量
    var unsafe1 = (*unsafe.Pointer)(unsafe.Pointer(&pP))

    // Old pointer
    var sy P

    // 为了演示效果先将unsafe1设置成Old Pointer
    px := atomic.SwapPointer(
        unsafe1, unsafe.Pointer(&sy))

    // 执行CAS操作,交换成功,结果返回true
    y := atomic.CompareAndSwapPointer(
        unsafe1, unsafe.Pointer(&sy), px)

    fmt.Println(y)
}

上面的示例并不是在并发环境下进行的CAS,只是为了演示效果,先把被操作数设置成了Old Pointer。
其实Mutex的底层实现也是依赖原子操作中的CAS实现的,原子操作的atomic包相当于是sync包里的那些同步原语的实现依赖。

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

type Mutex struct {
 state int32
 sema  uint32
}

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

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

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

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

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

func StoreInt32(addr *int32, val int32)

func StoreInt64(addr *int64, val int64)

func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

...

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

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

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

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

  • v.Store(c) - 写操作,将原始的变量c存放到一个atomic.Value类型的v里。
  • c := v.Load() - 读操作,从线程安全的v中读取上一步存放的内容。

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

简洁的接口使得它的使用也很简单,只需将需要做并发保护的变量读取和赋值操作用Load()和Store()代替就行了。
由于Load()返回的是一个interface{}类型,所以在使用前我们记得要先转换成具体类型的值,再使用。下面是一个简单的

例子演示atomic.Value的用法。

type Rectangle struct {
 length int
 width  int
}

var rect atomic.Value

func update(width, length int) {
 rectLocal := new(Rectangle)
 rectLocal.width = width
 rectLocal.length = length
 rect.Store(rectLocal)
}

func main() {
 wg := sync.WaitGroup{}
 wg.Add(10)
 // 10 个协程并发更新
 for i := 0; i < 10; i++ {
  go func() {
   defer wg.Done()
   update(i, i+5)
  }()
 }
 wg.Wait()
 _r := rect.Load().(*Rectangle)
 fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length)
}

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

总结

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

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

到此这篇关于详解Golang五种原子性操作的用法的文章就介绍到这了,更多相关详解Golang五种原子性操作的用法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Golang使用lua脚本实现redis原子操作

    目录 [redis 调用Lua脚本](#redis 调用Lua脚本) [redis+lua 实现评分排行榜实时更新](#redis+lua 实现评分排行榜实时更新) [lua 脚本](#lua 脚本) Golang调用redis+lua示例 byte切片与string的转换优化 redis 调用Lua脚本 EVAL命令 redis调用Lua脚本需要使用EVAL命令. redis EVAL命令格式: redis 127.0.0.1:6379> EVAL script numkeys key [ke

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

    目录 Go 语言提供了哪些原子操作 互斥锁跟原子操作的区别 比较并交换 atomic.Value保证任意值的读写安全 总结 本文我们详细聊一下Go语言的原子操作的用法,啥是原子操作呢?顾名思义,原子操作就是具备原子性的操作... 是不是感觉说了跟没说一样,原子性的解释如下: 一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) .这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态. CPU执行一系列操作时不可

  • 详解Golang使用MongoDB通用操作

    MongoDB是Nosql中常用的一种数据库,今天笔者就简单总结一下Golang如何使用这些通用的供能的,不喜勿喷...        研究的事例结构如下: type LikeBest struct { AuthorName string `bson:"authorname,omitempty"` BookName string `bson:"bookname,omitempty"` PublishTime string `bson:"publishtim

  • 详解Golang开启http服务的三种方式

    前言 都说go标准库实用,Api设计简洁.这次就用go 标准库中的net/http包实现一个简洁的http web服务器,包括三种版本. v1最简单版 直接使用http.HandleFunc(partern,function(http.ResponseWriter,*http.Request){}) HandleFunc接受两个参数,第一个为路由地址,第二个为处理方法. //v1 func main() { http.HandleFunc("/", func(w http.Respon

  • 详解Golang中的各种时间操作

    需求 时间格式的转换比较麻烦,自己写了个工具,可以通过工具中的这些方法相互调用转成自己想要的格式,代码如下,后续有新的函数再添加 实现代码 package utils import "time" const ( TIMEFORMAT = "20060102150405" NORMALTIMEFORMAT = "2006-01-02 15:04:05" ) // 当前时间 func GetTime() time.Time{ return time.

  • 详解Golang实现请求限流的几种办法

    简单的并发控制 利用 channel 的缓冲设定,我们就可以来实现并发的限制.我们只要在执行并发的同时,往一个带有缓冲的 channel 里写入点东西(随便写啥,内容不重要).让并发的 goroutine在执行完成后把这个 channel 里的东西给读走.这样整个并发的数量就讲控制在这个 channel的缓冲区大小上. 比如我们可以用一个 bool 类型的带缓冲 channel 作为并发限制的计数器. chLimit := make(chan bool, 1) 然后在并发执行的地方,每创建一个新

  • 详解Golang 与python中的字符串反转

    详解Golang 与python中的字符串反转 在go中,需要用rune来处理,因为涉及到中文或者一些字符ASCII编码大于255的. func main() { fmt.Println(reverse("Golang python")) } func reverse(src string) string { dst := []rune(src) len := len(dst) var result []rune result = make([]rune, 0) for i := le

  • 详解golang中的method

    什么是method(方法)?method是函数的另外一种形态,隶属于某个类型的方法. method的语法: func (r Receiver) funcName (parameters) (result) receiver可以看作是method的第一个参数,method并且支持继承和重写. Go中虽没有class,但依旧有method 通过显示说明receiver来实现与某个类型的结合 只能为同一个包中的类型定义方法 receiver可以是类型的值或者指针 不存在方法重载 可以使用值或指针来调用

  • 详解Golang并发操作中常见的死锁情形

    目录 第一种情形:无缓存能力的管道,自己写完自己读 第二种情形:协程来晚了 第三种情形:管道读写时,相互要求对方先读/写 第四种情形:读写锁相互阻塞,形成隐形死锁 什么是死锁,在Go的协程里面死锁通常就是永久阻塞了,你拿着我的东西,要我先给你然后再给我,我拿着你的东西又让你先给我,不然就不给你.我俩都这么想,这事就解决不了了. 第一种情形:无缓存能力的管道,自己写完自己读 先上代码: func main() { ch := make(chan int, 0) ​ ch <- 666 x := <

  • Go依赖注入DI工具wire使用详解(golang常用库包)

    目录 什么是依赖注入 第一次编写mysql操作类: 第二次编写mysql操作类: 第三次编写mysql操作类: 何时使用依赖注入 wire 概念说明 provider 和 injector provider injector wire 使用 快速开始 小结 绑定接口 Provider Set 参考 google 出品的依赖注入库 wire:https://github.com/google/wire 什么是依赖注入 依赖注入 ,英文全名是 dependency injection,简写为 DI.

  • 一文详解Golang 定时任务库 gron 设计和原理

    目录 cron 简介 gron 定时参数 源码解析 Cron Entry 按照时间排序 新增定时任务 启动和停止 Schedule 扩展性 经典写法-控制退出 结语 cron 简介 在 Unix-like 操作系统中,有一个大家都很熟悉的 cli 工具,它能够来处理定时任务,周期性任务,这就是: cron. 你只需要简单的语法控制就能实现任意[定时]的语义.用法上可以参考一下这个Crontab Guru Editor,做的非常精巧. 简单说,每一个位都代表了一个时间维度,* 代表全集,所以,上面

随机推荐