3.10 GMP 偷取 G 为什么不需要加锁?¶
在之前的文章中,想必你已经知道了 GMP 模型的工作原理,其中有一个非常重要的问题,这个问题非常的细节,但非常值得拿出来讲一下。
P 从全局队列里取 G 的时候,由于可能有多个 P 同时取G 的情况,因此需要加锁,这很容易理解。然后 P 从本地队列里取 G 的时候,正常情况下,只有自己取 G,不用加锁也没关系。但问题就在于当有其他的 P 处于自旋状态的时候,就有可能来自己这边偷。
如此看来,P 的本地队列也有并发竞争的问题,可为什么网上的文章都说从 P 本地队列里的时候,也不用加锁呢?
难道是这些人,瞎说的?
其实啊,这些人说的并没有错,只是他们没把事情说清楚。
原来 P 从本地队列取 G 的这个操作,是一个 CAS 操作,它具有原子性,是由硬件直接支持的,不需要并发的竞争关系。
而我们常见的加锁操作来避免并发的竞争问题,是从操作系统层面来实现的。
因此 GMP 中偷取 G 的过程也是不需要加锁的噢。
CAS 的原子操作虽然可以让程序员变得简单,有时也要付出一定的代价,使用 CAS 有两个小问题:
使用 CAS 为保证执行成功,程序需要用 for 循环不断尝试,直到成功才返回,因此如果 CAS 长时间不成功,就会阻塞其他硬件对CPU的访问,开销比较大。
每次使用 CAS 会原子操作时,一般只能对一个共享变量做操作,若要对多个共享变量操作,循环 CAS 可能就不太做,不过应该可以通过把多个变量放在一个对象里来进行 CAS 操作。
为了让你对 CAS 有一个直观的理解,这里直接放上网上找的一段使用 atomic 包实现的 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)
}