jasper的技术小窝

关注DevOps、运维监控、Python、Golang、开源、大数据、web开发、互联网

深入Golang之sync.mutex

作者:jasper | 分类:Golang | 标签:   | 阅读 73 次 | 发布:2017-11-20 10:03 p.m.

锁不管在操作系统里面,还是在各个编程语言里面都是非常重要的。特别是在Golang的goroutine中,会涉及到很多对同一个对象的操作,虽然考虑到锁对性能的影响,我们在Golang中会尽量使用channel来达到同样的效果,但是有时候还是不得不使用锁,现在来看看锁在Golang中的实现。

mutex的数据结构

type Mutex struct {
    state int32
    sema  uint32
}

const (
    mutexLocked = 1 << iota // mutex is locked
    mutexWoken
    mutexWaiterShift = iota
     starvationThresholdNs = 1e6
)

其中sema是信号量,用一个非负数来表示。state表示Mutex的状态。mutexLocked表示锁被别的goroutine占用,mutexWoken=2表示mutex被唤醒,mutexWaiterShift=2表示统计阻塞在该mutex上的goroutine数目需要移位的数值。starvationThresholdNs表示starvation的阈值,单位为ns。Mutex可以有两种操作模式,normal和starvation。在starvation模式下,mutex的所有权直接从解锁的goroutine转移到队列最前面的goroutine。新到达的goroutine即使马上要unlock了,也不会获取mutex,也不会自旋。相反,他们自己排队在等待队列的尾部。

获取锁

这部分我们分段来说,首先是调用atomic.CompareAndSwapInt32

    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        if race.Enabled {
            race.Acquire(unsafe.Pointer(m))
        }
        return
    }

是的,直接调用cas的操作,CompareAndSwapInt32()就是int32型数字的cas实现。cas(&addr, old, new)的意思是if addr==old, addr=new;具体在这里就是看当前锁的状态是否为0(0表示没有被锁),那么就把状态置为locked状态。

这里来多说一下cas的实现,最后是运行的汇编,不同的系统有不同的实现,在AMD64里面的实现:

TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0-17
    JMP ·CompareAndSwapUint32(SB)

TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0-17
    MOVQ    addr+0(FP), BP
    MOVL    old+8(FP), AX
    MOVL    new+12(FP), CX
    LOCK
    CMPXCHGL    CX, 0(BP)
    SETEQ   swapped+16(FP)
    RET

里面最重要的就是CMPXCHGL指令前加了个lock前缀
lock前缀的指令在多核处理器下会引发两件事:

1、将当前处理器缓存行的数据写回到系统内存。
2、这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

接下来继续看,是一个for循环,首先是来一个自旋,自旋的条件为:1.多核; 2.GOMAXPROCS>1; 3.至少有一个运行的P并且local的P队列为空。如果满足条件就会做自旋:

func sync_runtime_doSpin() {
    procyield(active_spin_cnt)
}

这里的自旋不会一直进行下去,每次的次数为active_spin_cnt=30,其中procyield的实现就是用汇编调用了指令cycles让CPU忙起来:

TEXT runtime·procyield(SB),NOSPLIT,$0-0
    MOVL    cycles+0(FP), AX
again:
    PAUSE
    SUBL    $1, AX
    JNZ again
    RET

接下来,有几个可以跳出for循环的条件:

这里就不细说了,感兴趣的可以研读源码。

下面我们来看下获取锁的时候对sema信号量的操作,具体函数为runtime_SemacquireMutex(&m.sema, queueLifo),作用其实就是将当前goroutine塞入信号量m.sema关联的goroutine waiting list,并休眠。其实就是实现一个链表,然后执行queue操作,注意在其中也会用到锁,但是不是mutex的,而是sema自己实现的一个简单的锁,感兴趣的可以去看看。

释放锁

释放锁的时候,首先得到一个new,其参数就是m.state-1之后的值,若是new为0,的表示,当前是lockde状态。接下来就是一段循环,我们仍然来看跳出循环的条件:

    if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                return
            }

如果阻塞在该锁上没有等待的goroutine或者有goroutine已经被唤醒了,或者抢占了锁亦或再也不用唤醒了,则跳出。

接下来就是释放锁的逻辑了:

new = (old - 1<<mutexWaiterShift) | mutexWoken
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
                runtime_Semrelease(&m.sema, false)
                return
            }
            old = m.state

可以看到和获取锁的差不多,仍然是通过atomic.CompareAndSwapInt32函数来置state的值,不过之前需要先将阻塞在mutex上的goroutine数目减一。

接下来runtime_Semrelease函数和上面的runtime_SemacquireMutex相反,执行dequeue操作。

读写锁

在锁里面还有专门为读写设计的读写锁RWMutex,好处就是不必要分别设置两个锁来控制,我们来看下数据结构吧:

type RWMutex struct {
    w           Mutex   
    writerSem   uint32 
    readerSem   uint32 
    readerCount int32  
    readerWait  int32
}

其实看了这个数据结构就大概知道其实现的机制了,底层还是互斥锁,但是使用sema来分别保存reader和writer。

其主要有四个方法:Lock,Unlock,RLock,RUnlock;其中前两个是写锁使用,后面两个为读锁使用。具体的实现其实都一样,只是runtime_Semacquireruntime_Semrelease的对象不一样,分别为writerSemreaderSem,具体的不赘述。

转载请注明出处:http://www.opscoder.info/golang_mutex.html

【上一篇】 深入Golang之goroutine
【下一篇】 深入Golang之sync.pool
其他分类: