syncMutexRWMutexWaitGroupOnceCondChannel

什么是CAS

在看源码实现之前,咱们先了解一下CAS。

维基百科定义:CAS是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而防止多线程同时改写某一数据时因为执行程序不确定性以及中断的不可预知性产生的数据不统一问题。 该操作通过将内存中的值与指定数据进行比拟,当数值一样时将内存中的数据替换为新的值。

CAS的实现思维能够用以下伪代码示意

bool Cas(int *val, int old, int new)
 Atomically:
    if(*val == old){
        *val = new;
        return 1;
    } else {
        return 0;
    }
sync/atomic/doc.goCompareAndSwapInt32
package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    a := int32(10)
    ok := atomic.CompareAndSwapInt32(&a, 10, 100)
    fmt.Println(a, ok)
    ok = atomic.CompareAndSwapInt32(&a, 10, 50)
    fmt.Println(a, ok)
}

它的执行后果如下

 $ go run main.go
100 true
100 false

CAS能做什么

CAS从线程层面来说,它是非阻塞的,其乐观地认为在数据更新期间没有其余线程影响,因而也经常被称为是一种轻量级的乐观锁。它关注的是并发平安,而并非并发同步。

sync.MutexLock
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.statem.state==0mutexLocked

源码解读

CompareAndSwapInt32sync/atomic/doc.go
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
sync/atomic/asm.s
TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0
    JMP    runtime∕internal∕atomic·Cas(SB)
JMPruntime∕internal∕atomic·Cas(SB)amd64runtime/internal/atomic/asm_amd64.s
TEXT runtime∕internal∕atomic·Cas(SB),NOSPLIT,$0-17
    MOVQ    ptr+0(FP), BX
    MOVL    old+8(FP), AX
    MOVL    new+12(FP), CX
    LOCK
    CMPXCHGL    CX, 0(BX)
    SETEQ    ret+16(FP)
    RET
Plan9Plan 9Plan 9

因为本文的重点并不是plan 9,所以这里就只解释上述汇编代码的含意。

atomic.Cas(SB)func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)addroldnewswapped
symbol+offset(FP)
ptr+0(FP)AXBXCXMOV X YXYMOVLQ
 MOVQ     ptr+0(FP), BX  // 第一个参数addr命名为ptr,放入BP(MOVQ,实现8个字节的复制)
 MOVL     old+8(FP), AX  // 第二个参数old,放入AX(MOVL,实现4个字节的复制)
 MOVL     new+12(FP), CX // 第三个参数new,放入CX(MOVL,实现4个字节的复制)
LOCK
Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal ensures that the processor has exclusive use of any shared memory while the signal is asserted.

在多处理器环境中,指令前缀LOCK可能确保,在执行LOCK随后的指令时,处理器领有对任何共享内存的独占应用。

LOCK:是一个指令前缀,其后必须跟一条“读-改-写”性质的指令,它们能够是ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG。该指令是一种锁定协定,用于封闭总线,禁止其余 CPU 对内存的操作来保障原子性。

在汇编代码里给指令加上 LOCK 前缀,这是CPU 在硬件层面反对的原子操作。但这样的锁粒度太粗,其余无关的内存操作也会被阻塞,大幅升高零碎性能,核数越多愈发显著。

为了进步性能,Intel 从 Pentium 486 开始引入了粒度较细的缓存锁:MESI协定(对于该协定,小菜刀在之前的文章《CPU缓存体系对Go程序的影响》有具体介绍过)。此时,只管有LOCK前缀,但如果对应数据曾经在 cache line里,也就不必锁定总线,仅锁住缓存行即可。

    LOCK
    CMPXCHGL    CX, 0(BX)
CMPXCHGLLAXold0(BX)ptrCXnew
 SETEQ    ret+16(FP)
 RET
SETEQCMPXCHGLCMPXCHGLretswappedRET

总结

atomic.CompareAndSwapInt32LOCKamd64

在Go提供的原子操作库atomic中,除了CAS还有许多有用的原子办法,它们独特筑起了Go同步原语体系的基石

func SwapIntX(addr *intX, new intX) (old intX)
func CompareAndSwapIntX(addr *intX, old, new intX) (swapped bool)
func AddIntX(addr *intX, delta intX) (new intX)
func LoadIntX(addr *uintX) (val uintX)
func StoreIntX(addr *intX, val intX)
func XPointer(addr *unsafe.Pointer, val unsafe.Pointer)

那么它们是如何实现的?小菜刀将它们实现的要害指令总结如下。

XCHGQLOCKCMPXCHGQLOCKXADDQMOVQXCHGQ
SwapStoreLOCK
If a memory operand is referenced, the processor’s locking protocol is automatically implemented for the duration of the exchange operation, regardless of the presence or absence of the LOCK prefix or of the value of the IOPL
SwapStoreLOCKXCHGQ
LoadStore/Swap