一, 前言
go 语言类似 Java JUC 包也提供了一些列用于多线程之间进行同步的措施, 比如低级的同步措施有 锁, CAS, 原子变量操作类. 相比 Java 来说 go 提供了独特的基于通道的同步措施. 本节我们先来看看 go 中 CAS 操作
二, CAS 操作
go 中的 Cas 操作与 java 中类似, 都是借用了 CPU 提供的原子性指令来实现. CAS 操作修改共享变量时候不需要对共享变量加锁, 而是通过类似乐观锁的方式进行检查, 本质还是不断的占用 CPU 资源换取加锁带来的开销 (比如上下文切换开销). 下面一个例子使用 CAS 来实现计数器
- package main
- import (
- "fmt"
- "sync"
- "sync/atomic"
- )
- var (
- counter int32 // 计数器
- wg sync.WaitGroup // 信号量
- )
- func main() {
- threadNum := 5
- //1. 五个信号量
- wg.Add(threadNum)
- //2. 开启 5 个线程
- for i := 0; i < threadNum; i++ {
- go incCounter(i)
- }
- //3. 等待子线程结束
- wg.Wait()
- fmt.Println(counter)
- }
- func incCounter(index int) {
- defer wg.Done()
- spinNum := 0
- for {
- //2.1 原子操作
- old := counter
- ok := atomic.CompareAndSwapInt32(&counter, old, old+1)
- if ok {
- break
- } else {
- spinNum++
- }
- }
- fmt.Printf("thread,%d,spinnum,%d\n",index,spinNum)
- }
如上代码 main 线程首先创建了 5 个信号量, 然后开启五个线程执行 incCounter 方法
incCounter 内部执行代码 2.1 使用 cas 操作递增 counter 的值, atomic.CompareAndSwapInt32 具有三个参数, 第一个是变量的地址, 第二个是变量当前值, 第三个是要修改变量为多少, 该函数如果发现传递的 old 值等于当前变量的值, 则使用第三个变量替换变量的值并返回 true, 否则返回 false.
这里之所以使用无限循环是因为在高并发下每个线程执行 CAS 并不是每次都成功, 失败了的线程需要重写获取变量当前的值, 然后重新执行 CAS 操作. 读者可以把线程数改为 10000 或者更多会发现输出 thread,5329,spinnum,1 其中 1 说明该线程尝试了两个 CAS 操作, 第二次才成功.
三, 总结
go 中 CAS 操作具有原子性, 在解决多线程操作共享变量安全上可以有效的减少使用锁所带来的开销, 但是这是使用 CPU 资源做交换的.
来源: https://yq.aliyun.com/articles/690418