Go|sync.mutex 源代码分析

sync.mutex 源代码分析

[TOC]

针对 Golang 1.10.3 的 sync.Mutex 进行分析,代码位置:sync/mutex.go

sync_mutex.jpeg

结构体定义


type Mutex struct {
    state int32  // 指代mutex锁当前的状态
    sema  uint32 // 信号量,用于唤醒goroutine
}

Mutex 中的 state 用于指代锁当前的状态,如下所示


1111 1111 ...... 1111 1111
\_________29__________/|||
 存储等待 goroutine 数量 
 
[0]第0个bit 表示当前 mutex 是否已被某个goroutine所拥有,1=加锁
[1]第1个bit 表示当前 mutex 是否被唤醒,也就是有某个唤醒的goroutine要尝试获取锁
[2]第2个bit 表示 mutex 当前是否处于饥饿状态

常量定义


const (
    mutexLocked = 1 << iota
    mutexWoken
    mutexStarving
    mutexWaiterShift = iota
    starvationThresholdNs = 1e6
)

  • mutexLocked 值为1,根据 mutex.state & mutexLocked 得到 mutex 的加锁状态,结果为1表示已加锁,0表示未加锁

  • mutexWoken 值为2(二进制:10),根据 mutex.state & mutexWoken 得到 mutex 的唤醒状态,结果为1表示已唤醒,0表示未唤醒

  • mutexStarving 值为4(二进制:100),根据 mutex.state & mutexStarving 得到 mutex 的饥饿状态,结果为1表示处于饥饿状态,0表示处于正常状态

  • mutexWaiterShift 值为3,根据 mutex.state >> mutexWaiterShift 得到当前等待的 goroutine 数目

  • starvationThresholdNs 值为1e6纳秒,也就是1毫秒,当等待队列中队首 goroutine 等待时间超过 starvationThresholdNs,mutex 进入饥饿模式

饥饿模式与正常模式

根据Mutex的注释,当前的Mutex有如下的性质。这些注释将极大的帮助我们理解Mutex的实现。

Mutex 有两种工作模式:正常模式和饥饿模式

正常模式

在正常模式中,所有等待锁的 goroutine 按照 FIFO 的顺序排队获取锁,但是一个被唤醒的等待者有时候并不能获取 mutex,它还需要和新到来的 goroutine 们竞争 mutex 的使用权。新到来的 goroutine 具有优势,它们已经在 CPU 上运行且它们数量很多,因此一个被唤醒的等待者有很大的概率获取不到锁。在这种情况下,这个被唤醒的 goroutine 会加入到等待队列的前面。如果一个等待的 goroutine 超过 1ms 没有获取锁,它就会将 mutex 切换到饥饿模式

饥饿模式

在饥饿模式中,锁的所有权将从 unlock 的 gorutine 直接交给交给等待队列中的第一个。新来的 goroutine 将不会尝试去获得锁,即使锁看起来是 unlock 状态, 也不会去尝试自旋操作,而是放在等待队列的尾部。

如果一个等待的goroutine获取了锁,并且满足一以下其中的任何一个条件,它会将锁的状态转换为正常状态。

  1. 它是等待队列中的最后一个;
  2. 它等待的时间少于1ms。

函数

在分析源代码之前, 我们要从多线程(goroutine)的并发场景去理解为什么实现中有很多的分支。

当一个goroutine获取这个锁的时候, 有可能这个锁根本没有竞争者, 那么这个goroutine轻轻松松获取了这个锁。

而如果这个锁已经被别的goroutine拥有, 就需要考虑怎么处理当前的期望获取锁的goroutine。

同时, 当并发goroutine很多的时候,有可能会有多个竞争者, 而且还会有通过信号量唤醒的等待者。

以下代码已经去掉了与核心代码无关的 race 代码。

Lock

Lock 方法申请对 mutex 加锁,Lock 执行的时候,分三种情况:

  1. 无冲突 通过 CAS 操作把当前状态设置为加锁状态
  2. 有冲突 开始自旋,并等待锁释放,如果其他 goroutine 在这段时间内释放了该锁,直接获得该锁;如果没有释放,进入3
  3. 有冲突,且已经过了自旋阶段 通过调用 semacquire 函数来让当前 goroutine 进入等待状态

func (m *Mutex) Lock() {
    // 如果 mutext 的 state 没有被锁,也没有等待/唤醒的 goroutine , 锁处于正常状态,那么获得锁,返回.
    // 比如锁第一次被 goroutine 请求时,就是这种状态。或者锁处于空闲的时候,也是这种状态。
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        return
    }

    var waitStartTime int64 // 当前 goroutine 开始等待的时间
    starving := false       // 当前 goroutine 是否已经处于饥饿状态
    awoke := false          // 当前 goroutine 是否被唤醒
    iter := 0               // 自旋迭代的次数
    old := m.state          // old 保存当前 mutex 的状态
    for {
        // 第一个条件是 state 已被锁,但是不是饥饿状态。如果是饥饿状态,自旋是没有用的,锁的拥有权直接交给了等待队列的第一个。
        // 第二个条件是还可以自旋,多核、压力不大并且在一定次数内可以自旋, 具体的条件可以参考`sync_runtime_canSpin`的实现(汇编实现,内部持续调用 PAUSE 指令,消耗 CPU 时间)。
        // 如果满足这两个条件,不断自旋来等待锁被释放、或者进入饥饿状态、或者不能再自旋。
        if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
            // 自旋的过程中如果发现 state 还没有设置 woken 标识,则设置它的 woken 标识, 并标记自己为被唤醒。
            if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                awoke = true
            }
            runtime_doSpin()
            iter++
            old = m.state
            continue
        }
        // 到了这一步, state的状态可能是:
        // 1. 锁还没有被释放,锁处于正常状态
        // 2. 锁还没有被释放, 锁处于饥饿状态
        // 3. 锁已经被释放, 锁处于正常状态
        // 4. 锁已经被释放, 锁处于饥饿状态
        // 并且本gorutine的 awoke可能是true, 也可能是false (其它goutine已经设置了state的woken标识)

        // new 复制 state的当前状态, 用来设置新的状态。old 是锁当前的状态
        new := old
        // 如果 old state 状态不是饥饿状态, new state 设置锁, 尝试通过CAS获取锁。
        // 如果 old state 状态是饥饿状态, 则不设置 new state 的锁,因为饥饿状态下锁直接转给等待队列的第一个.
        if old&mutexStarving == 0 {
            new |= mutexLocked
        }
        // 当 mutex 处于加锁状态或饥饿状态的时候,新到来的 goroutine 进入等待队列
        if old&(mutexLocked|mutexStarving) != 0 {
            new += 1 << mutexWaiterShift
        }
        // 当前 goroutine 将 mutex 切换为饥饿状态,但如果当前 mutex 未加锁,则不需要切换
        // Unlock 操作希望饥饿模式存在等待者
        if starving && old&mutexLocked != 0 {
            new |= mutexStarving
        }
        // 如果本 goroutine 已经设置为唤醒状态, 需要清除 new state 的唤醒标记,
        // 因为本 goroutine 要么获得了锁,要么进入休眠,总之state的新状态不再是woken状态.
        if awoke {
            if new&mutexWoken == 0 {
                throw("sync: inconsistent mutex state")
            }
            new &^= mutexWoken
        }
        // 调用 CAS 更新 state 状态
        // 注意 new 的锁标记不一定是 true, 也可能只是标记一下锁的 state 是饥饿状态.
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            // 如果 old state 的状态是未被锁状态,并且锁不处于饥饿状态,
            // 那么当前 goroutine 已经获取了锁的拥有权,返回
            if old&(mutexLocked|mutexStarving) == 0 {
                break
            }
            // 设置/计算本 goroutine 的等待时间
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
            // 既然未能获取到锁, 那么就使用 sleep 原语阻塞本 goroutine
            // 如果是新来的 goroutine, queueLifo=false, 加入到等待队列的尾部,耐心等待
            // 如果是唤醒的 goroutine, queueLifo=true, 加入到等待队列的头部
            runtime_SemacquireMutex(&m.sema, queueLifo)
            // 如果当前 goroutine 等待时间超过 starvationThresholdNs,mutex 进入饥饿模式
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            old = m.state
            // 如果当前的 state 已经是饥饿状态
            // 那么锁应该处于 Unlock 状态,锁被直接交给了本 goroutine
            if old&mutexStarving != 0 {
                // 如果当前的 state 已被锁,或者已标记为唤醒, 或者等待的队列中不为空,
                // 那么 state 是一个非法状态
                if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
                    throw("sync: inconsistent mutex state")
                }
                // 等待状态的 goroutine - 1
                delta := int32(mutexLocked - 1<<mutexWaiterShift)
                // 如果不是饥饿模式了或者当前等待着只剩下一个,退出饥饿模式
                if !starving || old>>mutexWaiterShift == 1 {
                    delta -= mutexStarving
                }
                // 更新状态
                // 因为已经获得了锁,退出、返回
                atomic.AddInt32(&m.state, delta)
                break
            }
            // 如果当前的锁是正常模式,本 goroutine 被唤醒,自旋次数清零,从 for 循环开始处重新开始
            awoke = true
            iter = 0
        } else {
            // 如果CAS不成功,重新获取锁的 state, 从 for 循环开始处重新开始
            old = m.state
        }
    }
}


Unlock

Unlock方法释放所申请的锁


func (m *Mutex) Unlock() {
    // 如果 state 不是处于锁的状态, 那么就是 Unlock 根本没有加锁的 mutex, panic
    new := atomic.AddInt32(&m.state, -mutexLocked)
    if (new+mutexLocked)&mutexLocked == 0 {
        throw("sync: unlock of unlocked mutex")
    }

    // 释放锁,并通知其它等待者
    // 锁如果处于饥饿状态,直接交给等待队列的第一个, 唤醒它,让它去获取锁
    // mutex 正常模式
    if new&mutexStarving == 0 {
        old := new
        for {
            // 如果没有等待者,或者已经存在一个 goroutine 被唤醒或得到锁、或处于饥饿模式
            // 直接返回.
            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                return
            }
            // 将等待的 goroutine-1,并设置 woken 标识
            new = (old - 1<<mutexWaiterShift) | mutexWoken
            // 设置新的 state, 这里通过信号量会唤醒一个阻塞的 goroutine 去获取锁.
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
                runtime_Semrelease(&m.sema, false)
                return
            }
            old = m.state
        }
    } else {
        // mutex 饥饿模式,直接将 mutex 拥有权移交给等待队列最前端的 goroutine
        // 注意此时 state 的 mutex 还没有加锁,唤醒的 goroutine 会设置它。
        // 在此期间,如果有新的 goroutine 来请求锁, 因为 mutex 处于饥饿状态, mutex 还是被认为处于锁状态,
        // 新来的 goroutine 不会把锁抢过去.
        runtime_Semrelease(&m.sema, true)
    }
}

sync.RWMutex 源码分析

RWMutex 是读写互斥锁,锁可以由任意数量的读取器或单个写入器来保持

RWMutex 的零值是一个解锁的互斥锁

RWMutex 是抢占式的读写锁,写锁之后来的读锁是加不上的

代码位置:sync/rwmutex.go

结构体定义


type RWMutex struct {
    w           Mutex  // 互斥锁
    writerSem   uint32 // 写锁信号量
    readerSem   uint32 // 读锁信号量
    readerCount int32  // 读锁计数器
    readerWait  int32  // 获取写锁时需要等待的读锁释放数量
}

常量定义


const rwmutexMaxReaders = 1 << 30   // 支持最多2^30个读锁

函数

以下是 sync.RWMutex 提供的4个方法

Lock

提供写锁加锁操作


func (rw *RWMutex) Lock() {
    // 使用 Mutex 锁
    rw.w.Lock()
    // 将当前的 readerCount 置为负数,告诉 RUnLock 当前存在写锁等待
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        // 等待读锁释放
        runtime_Semacquire(&rw.writerSem)
    }
}

Unlock

提供写锁释放操作


func (rw *RWMutex) Unlock() {
    // 加上 Lock 的时候减去的 rwmutexMaxReaders
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    // 没执行Lock调用Unlock,抛出异常
    if r >= rwmutexMaxReaders {
        race.Enable()
        throw("sync: Unlock of unlocked RWMutex")
    }
    // 通知当前等待的读锁
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false)
    }
    // 释放 Mutex 锁
    rw.w.Unlock()
}

RLock

提供读锁操作


func (rw *RWMutex) RLock() {
    // 每次 goroutine 获取读锁时,readerCount+1
    // 如果写锁已经被获取,那么 readerCount 在 -rwmutexMaxReaders 与 0 之间,这时挂起获取读锁的 goroutine
    // 如果写锁没有被获取,那么 readerCount > 0,获取读锁, 不阻塞
    // 通过 readerCount 判断读锁与写锁互斥, 如果有写锁存在就挂起goroutine, 多个读锁可以并行
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // 将 goroutine 排到G队列的后面,挂起 goroutine
        runtime_Semacquire(&rw.readerSem)
    }
}

RUnLock

对读锁进行解锁


func (rw *RWMutex) RUnlock() {
    // 写锁等待状态,检查当前是否可以进行获取
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        // r + 1 == 0表示直接执行RUnlock()
        // r + 1 == -rwmutexMaxReaders表示执行Lock()再执行RUnlock()
        // 两总情况均抛出异常
        if r+1 == 0 || r+1 == -rwmutexMaxReaders {
            race.Enable()
            throw("sync: RUnlock of unlocked RWMutex")
        }
        // 当读锁释放完毕后,通知写锁
        if atomic.AddInt32(&rw.readerWait, -1) == 0 {
            // The last reader unblocks the writer.
            runtime_Semrelease(&rw.writerSem, false)
        }
    }
}

总结

sync.Mutex

  • 同一个时刻只有一个线程能够拿到锁

注意:

  1. 不要重复锁定互斥锁
  2. 不要忘记解锁互斥锁
  3. 不要在多个函数之间直接传递互斥锁

sync.RWMutex

  • 如果设置了一个写锁,那么其它读的线程以及写的线程都拿不到锁,这个时候,与互斥锁的功能相同
  • 如果设置了一个读锁,那么其它写的线程是拿不到锁的,但是其它读的线程是可以拿到锁

读写互斥锁的实现比较有技巧性一些,需要几点

  • 读锁不能阻塞读锁,引入readerCount实现
  • 读锁需要阻塞写锁,直到所有读锁都释放,引入readerSem实现
  • 写锁需要阻塞读锁,直到所有写锁都释放,引入wirterSem实现
  • 写锁需要阻塞写锁,引入Metux实现
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容