30 | 原子操作(下)
我们接着上一篇文章的内容继续聊,上一篇我们提到了,sync/atomic包中的函数可以做的原子操作有:加法(add)、比较并交换(compare and swap,简称 CAS)、加载(load)、存储(store)和交换(swap)。并且以此衍生出了两个问题。
今天我们继续来看第三个衍生问题: 比较并交换操作与交换操作相比有什么不同?优势在哪里?
回答是:比较并交换操作即 CAS 操作,是有条件的交换操作,只有在条件满足的情况下才会进行值的交换。
所谓的交换指的是,把新值赋给变量,并返回变量的旧值。
在进行 CAS 操作的时候,函数会先判断被操作变量的当前值,是否与我们预期的旧值相等。如果相等,它就把新值赋给该变量,并返回true以表明交换操作已进行;否则就忽略交换操作,并返回false。
可以看到,CAS 操作并不是单一的操作,而是一种操作组合。这与其他的原子操作都不同。正因为如此,它的用途要更广泛一些。例如,我们将它与for语句联用就可以实现一种简易的自旋锁(spinlock)。
for {
if atomic.CompareAndSwapInt32(&num2, 10, 0) {
fmt.Println("The second number has gone to zero.")
break
}
time.Sleep(time.Millisecond * 500)
}
在for语句中的 CAS 操作可以不停地检查某个需要满足的条件,一旦条件满足就退出for循环。这就相当于,只要条件未被满足,当前的流程就会被一直“阻塞”在这里。
这在效果上与互斥锁有些类似。不过,它们的适用场景是不同的。我们在使用互斥锁的时候,总是假设共享资源的状态会被其他的 goroutine 频繁地改变。
而for语句加 CAS 操作的假设往往是:共享资源状态的改变并不频繁,或者,它的状态总会变成期望的那样。这是一种更加乐观,或者说更加宽松的做法。
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main() {
// 第三个衍生问题的示例。
forAndCAS1()
fmt.Println()
forAndCAS2()
}
// forAndCAS1 用于展示简易的自旋锁。
func forAndCAS1() {
sign := make(chan struct{}, 2)
num := int32(0)
fmt.Printf("The number: %d\n", num)
go func() { // 定时增加num的值。
defer func() {
sign <- struct{}{}
}()
for {
time.Sleep(time.Millisecond * 500)
newNum := atomic.AddInt32(&num, 2)
fmt.Printf("The number: %d\n", newNum)
if newNum == 10 {
break
}
}
}()
go func() { // 定时检查num的值,如果等于10就将其归零。
defer func() {
sign <- struct{}{}
}()
for {
if atomic.CompareAndSwapInt32(&num, 10, 0) {
fmt.Println("The number has gone to zero.")
break
}
time.Sleep(time.Millisecond * 500)
}
}()
<-sign
<-sign
}
// forAndCAS2 用于展示一种简易的(且更加宽松的)互斥锁的模拟。
func forAndCAS2() {
sign :