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