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

GoLangsync.Mutex实现分析

文章目录定义介绍模式转换简单模式正常模式饥饿模式阻塞与解锁加锁自旋定义介绍先从sync.Mutex定义出发,看看mutex是如何实现;sync.Mutex源码se


文章目录

    • 定义介绍
    • 模式转换
      • 简单模式
      • 正常模式
      • 饥饿模式
    • 阻塞与解锁
    • 加锁自旋


定义介绍

先从sync.Mutex定义出发,看看mutex是如何实现; sync.Mutex源码


  • sema: 信号量, 用于阻塞和唤醒goroutine; 注意, sema并不存储阻塞的goroutine的数量;
  • state: 状态(精彩之处); 它的使用被切分2部分,一部分是状态标识位,另一部分存储wait goroutine的数量;

在这里插入图片描述


源代码常量定义含义
mutexLocked锁定标志位001
mutexWoken唤醒标志位010
mutexStarving饥饿标志位100
mutexWaiterShift等待计数偏移3

所以,锁竞争可以理解为 当前goroutine是否将锁定标志位 设置成1; 正常模式是多goroutine抢占锁定标志位的过程;饥饿模式是饥饿标志位被设置成1;


模式转换

在这里插入图片描述

结合代码分析以上3种方式,实际是2种,即正常模式和饥饿模式;我这里将正常模式细分出简单模式和正常模式; 首先回忆一下锁的用法, 即

func xxxxx() {mu.Lock()defer mu.Unlock()......
}

当多个goroutine 抢锁时,成功的可以继续执行,失败的则被阻塞;等同于 抢锁成功的可以从Lock函数中返回,失败则留在Lock函数中无法返回。


简单模式


  • 加锁
  • 解锁

func (m *Mutex) Lock() {if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {...return}m.lockSlow()
}func (m *Mutex) Unlock() {...new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {m.unlockSlow(new)}
}

代码容易理解 当G抢锁时state=0变成state=1,加锁成功; 当G解锁时state=1变成state=0 解锁成功;


正常模式


  • 加锁
  • 解锁

func (m *Mutex) unlockSlow(new int32) {...if new&mutexStarving &#61;&#61; 0 {old :&#61; newfor {if old>>mutexWaiterShift &#61;&#61; 0 || old&(mutexLocked|mutexWoken|mutexStarving) !&#61; 0 {return}new &#61; (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old &#61; m.state}} else {...}
}

先看正常模式的解锁操作&#xff0c;已将饥饿模式代码隐藏; new &#61; m.state - mutexLocked, 意味着 锁定标志被解除; 正常模式解锁 会将state 唤醒标志置1, 并唤醒G

old :&#61; newfor {//old>>mutexWaiterShift &#61; 0 表示无等待的G; 这种情况可以直接返回; 因为没有需要唤醒的G;//old>>mutexWaiterShift > 0 表示存在等待G;////此时: old 可能为 ..1 000 或者 ..1 010; 因为已解锁且非饥饿////if old>>mutexWaiterShift &#61;&#61; 0 || old&(mutexLocked|mutexWoken|mutexStarving) !&#61; 0 {return}//// (old - 1<//// old 为 ...1 000 --> ...1 010//new &#61; (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old &#61; m.state}

runtime_Semrelease(&m.sema, false, 1) 队尾唤醒G
runtime_Semrelease(&m.sema, true, 1) 队首唤醒G

然后看一下正常模式的加锁操作&#xff1b;已将饥饿模式代码隐藏; 前文所过加锁成功可以等同于Lock函数返回&#xff0c;所以紧盯break; 接下来将分析如何退出;

func (m *Mutex) lockSlow() {var waitStartTime int64 //G等待时间纳秒starving :&#61; false //是否饥饿awoke :&#61; false //是否唤醒iter :&#61; 0 //自旋计数old :&#61; m.state //快照状态for {......new :&#61; old//非饥饿模式加锁 确保锁定标志为1if old&mutexStarving &#61;&#61; 0 {new |&#61; mutexLocked}//等待G计数增加if old&(mutexLocked|mutexStarving) !&#61; 0 {new &#43;&#61; 1 << mutexWaiterShift}// 进入饥饿模式if starving && old&mutexLocked !&#61; 0 {new |&#61; mutexStarving}//唤醒标志重置, 因为唤醒G时都会将唤醒标志置1if awoke {if new&mutexWoken &#61;&#61; 0 {throw("sync: inconsistent mutex state")}new &^&#61; mutexWoken}if atomic.CompareAndSwapInt32(&m.state, old, new) {if old&(mutexLocked|mutexStarving) &#61;&#61; 0 { //唤醒G抢锁成功 010 解锁时为010break // locked the mutex with CAS}//竞争失败重新等待; 新G将放入等待队列尾&#xff0c;唤醒G将放入等待队列首;queueLifo :&#61; waitStartTime !&#61; 0if waitStartTime &#61;&#61; 0 {waitStartTime &#61; runtime_nanotime()}runtime_SemacquireMutex(&m.sema, queueLifo, 1)//这个位置很关键; 它是唤醒G 开始执行的位置; 阻塞的底层是将G与P解耦&#xff0c;然后将G交由sema保存;////进入饥饿条件是等待时间 > 1msstarving &#61; starving || runtime_nanotime()-waitStartTime > starvationThresholdNs//唤醒G 需要重新快照state; 因为G已睡眠一段时间, state可能已改变;old &#61; m.stateif old&mutexStarving !&#61; 0 {...}//唤醒G 开启新一轮锁竞争awoke &#61; trueiter &#61; 0} else {old &#61; m.state}}...
}

饥饿模式

当唤醒G等待时间大于1ms时&#xff0c;即将进入模式&#xff0c;那么后续锁竞争的G 将直接进入等待队列&#xff0c;无需竞争;

func (m *Mutex) lockSlow() {var waitStartTime int64 //G等待时间纳秒starving :&#61; false //是否饥饿awoke :&#61; false //是否唤醒iter :&#61; 0 //自旋计数old :&#61; m.state //快照状态for {...new :&#61; old...//等待G计数增加if old&(mutexLocked|mutexStarving) !&#61; 0 {new &#43;&#61; 1 << mutexWaiterShift}// 进入饥饿模式if starving && old&mutexLocked !&#61; 0 {new |&#61; mutexStarving}...if atomic.CompareAndSwapInt32(&m.state, old, new) {....//竞争失败重新等待; 新G将放入等待队列尾&#xff0c;唤醒G将放入等待队列首;queueLifo :&#61; waitStartTime !&#61; 0if waitStartTime &#61;&#61; 0 {waitStartTime &#61; runtime_nanotime()}runtime_SemacquireMutex(&m.sema, queueLifo, 1)//这个位置很关键; 它是唤醒G 开始执行的位置; 阻塞的底层是将G与P解耦&#xff0c;然后将G交由sema保存;////进入饥饿条件是等待时间 > 1msstarving &#61; starving || runtime_nanotime()-waitStartTime > starvationThresholdNs//唤醒G 需要重新快照state; 因为G已睡眠一段时间, state可能已改变;old &#61; m.state//饥饿模式处理逻辑&#xff0c;有点绕;if old&mutexStarving !&#61; 0 {////没有等待G是不会进入饥饿模式的 old>>mutexWaiterShift &#61;&#61; 0////此时应该 ..1 100//if old&(mutexLocked|mutexWoken) !&#61; 0 || old>>mutexWaiterShift &#61;&#61; 0 {throw("sync: inconsistent mutex state")}delta :&#61; int32(mutexLocked - 1<<mutexWaiterShift)//// delta &#61; 1 - 8 &#61; -7 饥饿 1.....1 111; 符号位为1&#xff0c;后3位为111;//if !starving || old>>mutexWaiterShift &#61;&#61; 1 {//解除饥饿//-7-4 &#61; -11 &#61; 1...1 011 ////此时 0...1 100 &#43; 1...1 011 运算结果为 ...1 011 解除饥饿模式delta -&#61; mutexStarving}//等待G计数减1;//此时 0....1 100 &#43; 1....0 111 运算结果后 为 ...1 111, 仍然为饥饿模式atomic.AddInt32(&m.state, delta)break}...} else {old &#61; m.state}}...
}

阻塞与解锁

在这里插入图片描述


加锁自旋

自旋就是空跑阻止G进入阻塞队列; 因为G的唤醒和阻塞涉及G的调度, 调度是耗时的; 如果通过短暂的自选可以获取锁&#xff0c;那就避免G的调度耗时; 同时可以看到自旋会浪费CPU的;

func (m *Mutex) lockSlow() {...awoke :&#61; falseiter :&#61; 0old :&#61; m.statefor {if old&(mutexLocked|mutexStarving) &#61;&#61; mutexLocked && runtime_canSpin(iter) {if !awoke && old&mutexWoken &#61;&#61; 0 && old>>mutexWaiterShift !&#61; 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke &#61; true}runtime_doSpin()iter&#43;&#43;old &#61; m.statecontinue}new :&#61; old...}

runtime_canSpin&#xff0c;进入自旋的条件是严苛的&#xff0c;尽力避免浪费CPU;


  • 不能超过4次
  • 多核
  • 多P
  • 空闲待运行队列

如有不对之处&#xff0c;欢迎留言评论区


推荐阅读
  • 兆芯X86 CPU架构的演进与现状(国产CPU系列)
    本文详细介绍了兆芯X86 CPU架构的发展历程,从公司成立背景到关键技术授权,再到具体芯片架构的演进,全面解析了兆芯在国产CPU领域的贡献与挑战。 ... [详细]
  • JUC(三):深入解析AQS
    本文详细介绍了Java并发工具包中的核心类AQS(AbstractQueuedSynchronizer),包括其基本概念、数据结构、源码分析及核心方法的实现。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 本文是Java并发编程系列的开篇之作,将详细解析Java 1.5及以上版本中提供的并发工具。文章假设读者已经具备同步和易失性关键字的基本知识,重点介绍信号量机制的内部工作原理及其在实际开发中的应用。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 在Linux系统中,网络配置是至关重要的任务之一。本文详细解析了Firewalld和Netfilter机制,并探讨了iptables的应用。通过使用`ip addr show`命令来查看网卡IP地址(需要安装`iproute`包),当网卡未分配IP地址或处于关闭状态时,可以通过`ip link set`命令进行配置和激活。此外,文章还介绍了如何利用Firewalld和iptables实现网络流量控制和安全策略管理,为系统管理员提供了实用的操作指南。 ... [详细]
  • 【线段树】  本质是二叉树,每个节点表示一个区间[L,R],设m(R-L+1)2(该处结果向下取整)左孩子区间为[L,m],右孩子区间为[m ... [详细]
  • 使用方法:将要控制的角色拖到TargetBody,将相机的焦点拖到CamerPivot,,建议CameraPivot是一个放在TargetBody下的子物体,并且位置应该是在Tar ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • javax.mail.search.BodyTerm.matchPart()方法的使用及代码示例 ... [详细]
  • IOS Run loop详解
    为什么80%的码农都做不了架构师?转自http:blog.csdn.netztp800201articledetails9240913感谢作者分享Objecti ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 经过两天的努力,终于成功解决了半平面交模板题POJ3335的问题。原来是在`OnLeft`函数中漏掉了关键的等于号。通过这次训练,不仅加深了对半平面交算法的理解,还提升了调试和代码实现的能力。未来将继续深入研究计算几何的其他核心问题,进一步巩固和拓展相关知识。 ... [详细]
author-avatar
军校招生顾问天使
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有