图解Go里面的互斥锁mutex了解编程语言核心实现源码

1. 锁的基础概念

1.1 CAS与轮询

1.1.1 cas实现锁

image.png

在锁的实现中现在越来越多的采用CAS来进行,通过利用处理器的CAS指令来实现对给定变量的值交换来进行锁的获取

1.1.2 轮询锁

image.png

在多线程并发的情况下很有可能会有线程CAS失败,通常就会配合for循环采用轮询的方式去尝试重新获取锁

1.2 锁的公平性

image.png

锁从公平性上通常会分为公平锁和非公平锁,主要取决于在锁获取的过程中,先进行锁获取的线程是否比后续的线程更先获得锁,如果是则就是公平锁:多个线程按照获取锁的顺序依次获得锁,否则就是非公平性

1.3 饥饿与排队

1.3.1 锁饥饿

锁饥饿是指因为大量线程都同时进行获取锁,某些线程可能在锁的CAS过程中一直失败,从而长时间获取不到锁

1.3.2 排队机制

image.png

上面提到了CAS和轮询锁进行锁获取的方式,可以发现如果已经有线程获取了锁,但是在当前线程在多次轮询获取锁失败的时候,就没有必要再继续进行反复尝试浪费系统资源,通常就会采用一种排队机制,来进行排队等待

1.4 位计数

在大多数编程语言中针对实现基于CAS的锁的时候,通常都会采用一个32位的整数来进行锁状态的存储

2. mutex实现

2.1 成员变量与模式

2.1.1 成员变量

在go的mutex中核心成员变量只有两个state和sema,其通过state来进行锁的计数,而通过sema来实现排队

type Mutex struct {
    state int32
    sema  uint32
}

2.1.2 锁模式

锁模式主要分为两种

描述 公平性
正常模式 正常模式下所有的goroutine按照FIFO的顺序进行锁获取,被唤醒的goroutine和新请求锁的goroutine同时进行锁获取,通常新请求锁的goroutine更容易获取锁
饥饿模式 饥饿模式所有尝试获取锁的goroutine进行等待排队,新请求锁的goroutine不会进行锁获取,而是加入队列尾部等待获取锁

上面可以看到其实在正常模式下,其实锁的性能是最高的如果多个goroutine进行锁获取后立马进行释放则可以避免多个线程的排队消耗
同理在切换到饥饿模式后,在进行锁获取的时候,如果满足一定的条件也会切换回正常模式,从而保证锁的高性能

2.2 锁计数

2.2.1 锁状态

image.png

在mutex中锁有三个标志位,其中其二进制位分别位001(mutexLocked)、010(mutexWoken)、100(mutexStarving), 注意这三者并不是互斥的关系,比如一个锁的状态可能是锁定的饥饿模式并且已经被唤醒

    mutexLocked = 1 << iota // mutex is locked
    mutexWoken
    mutexStarving

2.2.2 等待计数

image.png

mutex中通过低3位存储了当前mutex的三种状态,剩下的29位全部用来存储尝试正在等待获取锁的goroutine的数量

    mutexWaiterShift = iota // 3

2.3唤醒机制

2.3.1 唤醒标志

image.png

唤醒标志其实就是上面说的第二位,唤醒标志主要用于标识当前尝试获取goroutine是否有正在处于唤醒状态的,记得上面公平模式下,当前正在cpu上运行的goroutine可能会先获取到锁

2.3.2 唤醒流程

image.png

当释放锁的时候,如果当前有goroutine正在唤醒状态,则只需要修改锁状态为释放锁,则处于woken状态的goroutine就可以直接获取锁,否则则需要唤醒一个goroutine, 并且等待这个goroutine修改state状态为mutexWoken,才退出

2.4 加锁流程

image.png

2.3.1 快速模式

如果当前没有goroutine加锁,则并且直接进行CAS成功,则直接获取锁成功

        // Fast path: grab unlocked mutex.
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        if race.Enabled {
            race.Acquire(unsafe.Pointer(m))
        }
        return
    }

2.3.2 自旋与唤醒

    // 注意这里其实包含两个信息一个是如果当前已经是锁定状态,然后允许自旋iter主要是计数次数实际上只允许自旋4次
    // 其实就是在自旋然后等待别人释放锁,如果有人释放锁,则会立刻进行下面的尝试获取锁的逻辑   
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
            // !awoke 如果当前线程不处于唤醒状态
            // old&mutexWoken == 0如果当前没有其他正在唤醒的节点,就将当前节点处于唤醒的状态
            // old>>mutexWaiterShift != 0 :右移3位,如果不位0,则表明当前有正在等待的goroutine
            // atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken)设置当前状态为唤醒状态
            if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
                atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
                awoke = true
            }
            // 尝试自旋,
            runtime_doSpin()
            // 自旋计数
            iter++
        // 从新获取状态
            old = m.state
            continue
        }

2.3.3 更改锁状态

流程走到这里会有两种可能:
1.锁状态当前已经不是锁定状态
2.自旋超过指定的次数,不再允许自旋了

        new := old
        if old&mutexStarving == 0 {
            // 如果当前不是饥饿模式,则这里其实就可以尝试进行锁的获取了|=其实就是将锁的那个bit位设为1表示锁定状态
            new |= mutexLocked
        }
        if old&(mutexLocked|mutexStarving) != 0 {
            // 如果当前被锁定或者处于饥饿模式,则增等待一个等待计数
            new += 1 << mutexWaiterShift
        }
        if starving && old&mutexLocked != 0 {
            // 如果当前已经处于饥饿状态,并且当前锁还是被占用,则尝试进行饥饿模式的切换
            new |= mutexStarving
        }
        if awoke {
            if new&mutexWoken == 0 {
                throw("sync: inconsistent mutex state")
            }
            // awoke为true则表明当前线程在上面自旋的时候,修改mutexWoken状态成功
            // 清除唤醒标志位
            // 为什么要清除标志位呢?
            // 实际上是因为后续流程很有可能当前线程会被挂起,就需要等待其他释放锁的goroutine来唤醒
            // 但如果unlock的时候发现mutexWoken的位置不是0,则就不会去唤醒,则该线程就无法再醒来加锁
            new &^= mutexWoken
        }

2.3.3 加锁排队与状态转换

再加锁的时候实际上只会有一个goroutine加锁CAS成功,而其他线程则需要重新获取状态,进行上面的自旋与唤醒状态的重新计算,从而再次CAS

        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            if old&(mutexLocked|mutexStarving) == 0 {
                // 如果原来的状态等于0则表明当前已经释放了锁并且也不处于饥饿模式下
                // 实际的二进制位可能是这样的 1111000, 后面三位全是0,只有记录等待goroutine的计数器可能会不为0
                // 那就表明其实
                break // locked the mutex with CAS
            }
            // 排队逻辑,如果发现waitStatrTime不为0,则表明当前线程之前已经再排队来,后面可能因为
            // unlock被唤醒,但是本次依旧没获取到锁,所以就将它移动到等待队列的头部
            queueLifo := waitStartTime != 0
            if waitStartTime == 0 {
                waitStartTime = runtime_nanotime()
            }
            // 这里就会进行排队等待其他节点进行唤醒
            runtime_SemacquireMutex(&m.sema, queueLifo)
            // 如果等待超过指定时间,则切换为饥饿模式 starving=true
            // 如果一个线程之前不是饥饿状态,并且也没超过starvationThresholdNs,则starving为false
            // 就会触发下面的状态切换
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            // 重新获取状态
            old = m.state
            if old&mutexStarving != 0 { 
                // 如果发现当前已经是饥饿模式,注意饥饿模式唤醒的是第一个goroutine
                // 当前所有的goroutine都在排队等待
            // 一致性检查,
                if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
                    throw("sync: inconsistent mutex state")
                }
                // 获取当前的模式
                delta := int32(mutexLocked - 1<<mutexWaiterShift)
                if !starving || old>>mutexWaiterShift == 1 {
                    // 如果当前goroutine不是饥饿状态,就从饥饿模式切换会正常模式
                    // 就从mutexStarving状态切换出去
                    delta -= mutexStarving
                }
                // 最后进行cas操作
                atomic.AddInt32(&m.state, delta)
                break
            }
            // 重置计数
            awoke = true
            iter = 0
        } else {
            old = m.state
        }

2.5 释放锁逻辑

image.png

2.5.1 释放锁代码

func (m *Mutex) Unlock() {
    if race.Enabled {
        _ = m.state
        race.Release(unsafe.Pointer(m))
    }

    // 直接进行cas操作
    new := atomic.AddInt32(&m.state, -mutexLocked)
    if (new+mutexLocked)&mutexLocked == 0 {
        throw("sync: unlock of unlocked mutex")
    }
    if new&mutexStarving == 0 {
        // 如果释放锁并且不是饥饿模式
        old := new
        for {

            if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
                // 如果已经有等待者并且已经被唤醒,就直接返回
                return
            }
            // 减去一个等待计数,然后将当前模式切换成mutexWoken
            new = (old - 1<<mutexWaiterShift) | mutexWoken
            if atomic.CompareAndSwapInt32(&m.state, old, new) {
                // 唤醒一个goroutine
                runtime_Semrelease(&m.sema, false)
                return
            }
            old = m.state
        }
    } else {
        // 唤醒等待的线程
        runtime_Semrelease(&m.sema, true)
    }
}

本文由博客一文多发平台 OpenWrite 发布!

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

推荐阅读更多精彩内容