热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

GoMutex源码学习

概述互斥锁是并发程序中对共享资源进行访问控制的主要手段,Mutex是go语言提供的简单易用的互斥锁。Mutex的结构很简单,暴露的方法也只有2个,一个加锁一个解锁。那么我们每天用的

概述

互斥锁是并发程序中对共享资源进行访问控制的主要手段,Mutex是go语言提供的简单易用的互斥锁。Mutex的结构很简单,暴露的方法也只有2个,一个加锁 一个解锁。那么我们每天用的Mutex互斥锁是如何实现的呢?
其实使用的是go语言automic包中的院子操作,具体如何使用可以参考之前写的文章。
在Mutex中的state是状态码,在mutex中把state分成4段。如下图:
《Go Mutex 源码学习》

  • Locked:表示是否上锁 上锁为1 未上锁为0
  • Woken:表示是否被唤醒,唤醒为1 未唤醒为0
  • Starving:表示是否为饥饿模式,饥饿模式为1 非饥饿模式为0
  • waiter:剩余的29位则为等待的goroutine数量

互斥锁的实现其实就是争夺Locked,当goroutineA 抢到了锁之后,第二个GoroutineB获取锁则会被阻塞等到GoroutineA释放锁之后GoroutineB将会被唤醒。当然具体实现则不会有这么简单,其中还有饥饿模式,自旋函数等一些概念。

前置概念

自旋

什么是自旋

加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续的探测Locked位是否变为0,这个过程即为自旋过程。自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。
自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的切换。

自旋的问题

如果自旋过程中获得锁,则马上执行该goroutine。如果永远在自旋模式中那么之前阻塞的goroutine则很难获得锁,这样一来一些goroutine则会被阻塞时间过长。如何解决这个问题,go mutex中引入了两种模式,具体请看下文。

Mutex的两种模式

普通模式

在普通模式下等待者以 FIFO 的顺序排队来获取锁,但被唤醒的等待者发现并没有获取到 mutex,并且还要与新到达的 goroutine 们竞争 mutex 的所有权。

饥饿模式

在饥饿模式下,mutex 的所有权直接从对 mutex 执行解锁的 goroutine 传递给等待队列前面的等待者。新到达的 goroutine 们不要尝试去获取 mutex,即使它看起来是在解锁状态,也不要试图自旋。

源码分析

Mutex对象

type Mutex struct {
// 状态码
state int32
// 信号量,用于向处于 Gwaitting 的 G 发送信号
sema uint32
}
const(
// 值=1 表示是否锁住 1=锁 0=未锁
mutexLocked = 1 < // 值=2 表示是否被唤醒 1=唤醒 0=未唤醒
mutexWoken
// 是否为饥渴模式(等待超过1秒则为饥渴模式)
mutexStarving
// 右移3位,为等待的数量
mutexWaiterShift = iota
// 饥饿模式的时间
starvatiOnThresholdNs= 1e6
)

加锁

func (m *Mutex) Lock() {
// 利用atomic包中的cas操作判断是否上锁
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
// 判断是否启用了race检测
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
// m.state = 1 直接返回 其他goroutine调用lock会发现已经被上锁
return
} // 等待时间
var waitStartTime int64
// 饥饿模式标志位
starving := false
// 唤醒标志位
awoke := false
// 自旋迭代的次数
iter := 0
// 保存 mutex 当前状态
old := m.state
// 循环
for {
// 判断 如果不是饥饿模式并且是否能够执行自旋函数(判断自旋次数)
// old&(0001|0100) == 0001 ==> old&0101
// 当old为0001为非饥饿模式 0001 == 0001 true 当old为0101饥饿模式 0101 == 0001 false
// runtime_canSpin 判断自旋少于4次,并且是多核机器上并且GOMAXPROCS>1
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 判断条件:
// 未被唤醒 && 等待数量不为0 && 使用CAS设置状态为已唤醒
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
// 设置激活为true
awoke = true
}
// 自旋函数 自旋次数+1
runtime_doSpin()
iter++
old = m.state
continue
}

// 如果不能执行自旋函数 记录一个new状态 然后判断改变new 最终使用CAS替换尝试设置state属性
new := old
// 当前的mutex.state处于正常模式,则将new的锁位设置为1
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 如果当前锁锁状态为锁定状态或者处于饥饿模式,则将等待的线程数量+1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 < }
// 如果starving变量为true并且处于锁定状态,则new的饥饿状态位打开
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// 对于状态的验证
if awoke {
// The goroutine has been woken from sleep,
// so we need to reset the flag in either case.
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
new &^= mutexWoken
}
// new已经判断设置完,如果mutex的state没有变动过的话 则替换成new
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果未被锁定并且并不是出于饥饿状态 退出循环 goroutine获取到锁
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// 如果当前的 goroutine 之前已经在排队了,就排到队列的前面。
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 进入休眠状态,等待信号唤醒后重新开始循环 如果queueLifo为true,则将等待goroutine插入到队列的前面
runtime_SemacquireMutex(&m.sema, queueLifo)
// 计算等待时间 确定 mutex 当前所处模式
// 此时这个goroutine已经被唤醒
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 判断被唤醒的goroutine是否为饥饿状态
if old&mutexStarving != 0 {
// If this goroutine was woken and mutex is in starvation mode,
// ownership was handed off to us but mutex is in somewhat
// inconsistent state: mutexLocked is not set and we are still
// accounted as waiter. Fix that.
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1< // 当不是饥饿状态或者等待数只有一个,则退出饥饿模式
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
// 如果不是饥饿模式 让新到来的 goroutine 先获取锁,继续循环
awoke = true
iter = 0
} else {
// 如果CAS替换未能成功 则继续循环
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}

解锁

func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 利用原子操作 设置state锁位置为0
new := atomic.AddInt32(&m.state, -mutexLocked)
// 判断状态,给未加锁的mutex解锁,抛出错误
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// 判断是否为饥饿模式
if new&mutexStarving == 0 {
// 正常状态
old := new
for {
// 如果等待的goroutine为零 || 已经被锁定、唤醒、或者已经变成饥饿状态
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 更新new的值,减去等待数量
new = (old - 1< // 使用CAS 替换旧值
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果替换成功 则恢复挂起的goroutine.r如果为 true表明将唤醒第一个阻塞的goroutine
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
} else {
// 恢复挂起的goroutine.r如果为 true表明将唤醒第一个阻塞的goroutine
runtime_Semrelease(&m.sema, true)
}
}

推荐阅读
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • MySQL数据库锁机制及其应用(数据库锁的概念)
    本文介绍了MySQL数据库锁机制及其应用。数据库锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,数据是一种供许多用户共享的资源,如何保证数据并发访问的一致性和有效性是数据库必须解决的问题。MySQL的锁机制相对简单,不同的存储引擎支持不同的锁机制,主要包括表级锁、行级锁和页面锁。本文详细介绍了MySQL表级锁的锁模式和特点,以及行级锁和页面锁的特点和应用场景。同时还讨论了锁冲突对数据库并发访问性能的影响。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
author-avatar
税钱AA
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有