Go语言设计与实现 -- Mutex源码剖析

news2024/10/6 12:31:35

golang-basic-sync-primitives

上图来自面向信仰编程

上图中,第一列为常见的同步原语,第二列为容器,第三列为互斥锁。

接下来我们来逐一介绍一下:

Mutex

我们先来看一下sync.Mutex的结构体:

type Mutex struct {
   // 当前互斥锁的状态
   state int32
   // 用于控制锁状态的信号量
   sema  uint32
}

状态

请添加图片描述

最低三位分别表示mutexLocked(互斥锁的锁定状态),mutexWoken(被从正常模式唤醒),mutexStarving(当前互斥锁进入饥饿状态),剩余位置用来表示当前有多少Goroutine在等待互斥锁的释放。

我们上面的介绍中引出了两个概念:正常模式饥饿模式。

正常模式和饥饿模式

正常模式是非公平锁。饥饿模式是公平锁。

刚开始的时候是处于正常模式的,也就是当一个G1持有一个锁的时候,G2会自旋的去尝试获取这个锁。

自旋超过4次还没有获取到锁的时候,G2就会被加入到锁的等待队列里面去,并阻塞等待被唤醒。

正常模式下,所有等待所的Goroutine按照FIFO的顺序等待。唤醒的Goroutine不会直接拥有锁,而是会和新的请求所的Goroutine竞争锁。但是新请求锁的Goroutine是具有优势的:它在CPU上执行,而且可能有好几个,所以刚刚唤醒的Goroutine有很大可能在锁竞争中失败,长时间获取不到锁,就会进入饥饿模式。

因此一旦Goroutine超过1ms没有获取到锁,它就会将当前互斥锁切换到饥饿模式,防止部分Goroutine被饿死。

在饥饿模式下,互斥锁会直接交给等待队列最前面的Goroutine。新创建的Goroutine在该状态下不能获取锁,也不会进入自旋状态,它们只会在队列末尾等待。如果一个Gorouine获得了互斥锁并且它在队列末尾或者它等待的时间少于1ms,那么当前互斥锁就会切换回正常模式。

正常模式下的互斥锁可以获得更高的性能,但是饥饿模式下的能避免由于陷入等待无法获取锁而造成的高尾延迟。

加锁和解锁

我们来看一下加锁的源码:

func (m *Mutex) Lock() {
   // Fast path: grab unlocked mutex.
   if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
      if race.Enabled {
         race.Acquire(unsafe.Pointer(m))
      }
      return
   }
   // Slow path (outlined so that the fast path can be inlined)
   m.lockSlow()
}

有一句代码非常重要:

atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)

这一句代码叫做CAS(compare and swap)。CAS是原子的,原因是它是由硬件指令完成的。

// CompareAndSwapInt32 executes the compare-and-swap operation for an int32 value.
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

假设我有内存中的原数据addr,旧的期望值old,需要修改的新值new,那么CAS的大致流程如下:

  • 比较addrold,查看是否相等
  • 如果相等的话,那么就把new写入addr,替代原来的old,并且返回true
  • 否则就返回false,不执行任何操作

这个操作的本质是:检测在两个操作之间有没有其他go程掺杂了操作,如果掺杂了那么操作无效,如果没有掺杂,那么继续操作就可以了

各种各样的锁都会被CAS实现。

然后回到上面的源码,我们继续来剖析:

如果互斥锁的状态不是0,那么CAS就会返回false,从而执行函数lockSlow()

我们来分析一下这个函数:

func (m *Mutex) lockSlow() {
   var waitStartTime int64
   starving := false
   awoke := false
   iter := 0
   old := m.state
   for {
      // Don't spin in starvation mode, ownership is handed off to waiters
      // so we won't be able to acquire the mutex anyway.
      if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
         // Active spinning makes sense.
         // Try to set mutexWoken flag to inform Unlock
         // to not wake other blocked goroutines.
         if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
            atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
            awoke = true
         }
         runtime_doSpin()
         iter++
         old = m.state
         continue
      }
      new := old
      // Don't try to acquire starving mutex, new arriving goroutines must queue.
      if old&mutexStarving == 0 {
         new |= mutexLocked
      }
      if old&(mutexLocked|mutexStarving) != 0 {
         new += 1 << mutexWaiterShift
      }
      // The current goroutine switches mutex to starvation mode.
      // But if the mutex is currently unlocked, don't do the switch.
      // Unlock expects that starving mutex has waiters, which will not
      // be true in this case.
      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
      }
      if atomic.CompareAndSwapInt32(&m.state, old, new) {
         if old&(mutexLocked|mutexStarving) == 0 {
            break // locked the mutex with CAS
         }
         // If we were already waiting before, queue at the front of the queue.
         queueLifo := waitStartTime != 0
         if waitStartTime == 0 {
            waitStartTime = runtime_nanotime()
         }
         runtime_SemacquireMutex(&m.sema, queueLifo, 1)
         starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
         old = m.state
         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<<mutexWaiterShift)
            if !starving || old>>mutexWaiterShift == 1 {
               // Exit starvation mode.
               // Critical to do it here and consider wait time.
               // Starvation mode is so inefficient, that two goroutines
               // can go lock-step infinitely once they switch mutex
               // to starvation mode.
               delta -= mutexStarving
            }
            atomic.AddInt32(&m.state, delta)
            break
         }
         awoke = true
         iter = 0
      } else {
         old = m.state
      }
   }

   if race.Enabled {
      race.Acquire(unsafe.Pointer(m))
   }
}

这个函数做了以下几个事情:

  • 判断当前Goroutine能否进入自旋
  • 通过自旋等待互斥锁的释放
  • 计算互斥锁的最新状态
  • 更新互斥锁的状态并获取锁

我们先来看第一部分,互斥锁是如何判断当前Goroutine能否进入自旋等待互斥锁的释放的:

var waitStartTime int64
starving := false
awoke := false
iter := 0
old := m.state
for {
   // Don't spin in starvation mode, ownership is handed off to waiters
   // so we won't be able to acquire the mutex anyway.
   if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
      // Active spinning makes sense.
      // Try to set mutexWoken flag to inform Unlock
      // to not wake other blocked goroutines.
      if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
         atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
         awoke = true
      }
      runtime_doSpin()
      iter++
      old = m.state
      continue
   }

进入自旋的条件是:

old := m.state
old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter)

这句话抽象出来的意思就是:

  • 互斥锁只有在普通模式下才能够进入自旋
  • runtime_canSpin必须返回true

然后我们来看runtime_canSpin在什么条件下才能返回true

  • 在有多个CPU的机器上运行
  • 当前Goroutine为了获取该锁进入自旋的次数少于4
  • 当前机器上至少存在一个正在运行的处理器P并且运行队列为空

可以看到条件非常苛刻,不过这也情有可原,因为自旋的过程会一直保持CPU的占用,持续检查某一个条件是否为真。使用不当会拖慢程序。

处理完自旋的特殊逻辑之后,互斥锁会根据上下文计算(只是计算,还没有更新)当前互斥锁的最新状态,会更新state字段中存储的不同信息。

new := old
// Don't try to acquire starving mutex, new arriving goroutines must queue.
if old&mutexStarving == 0 {
   new |= mutexLocked
}
if old&(mutexLocked|mutexStarving) != 0 {
   new += 1 << mutexWaiterShift
}
// The current goroutine switches mutex to starvation mode.
// But if the mutex is currently unlocked, don't do the switch.
// Unlock expects that starving mutex has waiters, which will not
// be true in this case.
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
}

计算了互斥锁状态之后,会使用CAS函数更新状态:

if atomic.CompareAndSwapInt32(&m.state, old, new) {
   if old&(mutexLocked|mutexStarving) == 0 {
      break // 通过CAS获取了锁
   }
   // If we were already waiting before, queue at the front of the queue.
   queueLifo := waitStartTime != 0
   if waitStartTime == 0 {
      waitStartTime = runtime_nanotime()
   }
   runtime_SemacquireMutex(&m.sema, queueLifo, 1)
   starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
   old = m.state
   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<<mutexWaiterShift)
      if !starving || old>>mutexWaiterShift == 1 {
         // Exit starvation mode.
         // Critical to do it here and consider wait time.
         // Starvation mode is so inefficient, that two goroutines
         // can go lock-step infinitely once they switch mutex
         // to starvation mode.
         delta -= mutexStarving
      }
      atomic.AddInt32(&m.state, delta)
      break
   }
   awoke = true
   iter = 0
} else {
   old = m.state
}

然后我们来看看解锁过程,解锁过程相比加锁过程稍微简单一点:

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

   // Fast path: drop lock bit.
   new := atomic.AddInt32(&m.state, -mutexLocked)
   if new != 0 {
      // Outlined slow path to allow inlining the fast path.
      // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
      m.unlockSlow(new)
   }
}

先调用函数atomic.AddInt32(&m.state, -mutexLocked)进行快速解锁。

如果返回值等于0,那么快速解锁成功。

如果不等于0,那么就调用 m.unlockSlow(new)进行慢速解锁。

func (m *Mutex) unlockSlow(new int32) {
   if (new+mutexLocked)&mutexLocked == 0 {
      throw("sync: unlock of unlocked mutex")
   }
   if new&mutexStarving == 0 {
      old := new
      for {
         // If there are no waiters or a goroutine has already
         // been woken or grabbed the lock, no need to wake anyone.
         // In starvation mode ownership is directly handed off from unlocking
         // goroutine to the next waiter. We are not part of this chain,
         // since we did not observe mutexStarving when we unlocked the mutex above.
         // So get off the way.
         if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
            return
         }
         // Grab the right to wake someone.
         new = (old - 1<<mutexWaiterShift) | mutexWoken
         if atomic.CompareAndSwapInt32(&m.state, old, new) {
            runtime_Semrelease(&m.sema, false, 1)
            return
         }
         old = m.state
      }
   } else {
      // Starving mode: handoff mutex ownership to the next waiter, and yield
      // our time slice so that the next waiter can start to run immediately.
      // Note: mutexLocked is not set, the waiter will set it after wakeup.
      // But mutex is still considered locked if mutexStarving is set,
      // so new coming goroutines won't acquire it.
      runtime_Semrelease(&m.sema, true, 1)
   }
}

我们来看一下这个函数:

  • 首先会校验锁状态的合法性,如果当前互斥锁已经被解锁了,会直接抛异常终止程序
  • 然后进行判断,如果是正常模式的话进行一套处理,饥饿模式进行另外一套处理。
  • 当互斥锁处于饥饿模式时,将锁的所有权交给队列中的下一个等待者,等待者会负责设置 mutexLocked 标志位;
  • 当互斥锁处于普通模式时,如果没有 Goroutine 等待锁的释放或者已经有被唤醒的 Goroutine 获得了锁,会直接返回;在其他情况下会通过 sync.runtime_Semrelease 唤醒对应的 Goroutine;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/124206.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第一天 java : 704.二分查找法、27.移除算法

文章目录Leecode 704.二分查找题目连接&#xff1a;[Leecode 704.二分查找](https://leetcode.cn/problems/remove-element/)遇到的问题题目二分法的第一种写法 &#xff08;左闭右闭)第二种解法&#xff08;左闭右开 代码呈现&#xff09;Leecode 27.移除元素题目链接&#xf…

MyBatis【创建与使用】

MyBatis【创建与使用】&#x1f34e;一. MyBatis&#x1f352;1.1. MyBatis 是什么&#xff1f;&#x1f352;1.2 没有使用MyBatis时的操作流程&#x1f352;1.3 MyBatis的操作与数据库之间的流程&#x1f34e;二.创建MyBatis项目&#x1f352;2.1 idea创建&#x1f352;2.2 配…

【Git】一文带你入门Git分布式版本控制系统(撤销修改、删除文件)

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;也会涉及到服务端 &#x1f4c3;个人状态&#xff1a; 在校大学生一枚&#xff0c;已拿多个前端 offer&#xff08;秋招&#xff09; &#x1f680;未…

Debezium故障演练

1、搭建演练环境 postgresql及wal2json插件安装:https://blog.csdn.net/li281037846/article/details/128411222 kafka及kafka-connect安装&#xff0c;略 //添加debezium connector curl -i -X POST -H "Content-Type:application/json" -H "Accepted:applic…

Qt样式(qss)使用小结(软件换肤,比如暗黑模式)

1.背景&#xff1a; Qt style sheet&#xff08;qss&#xff09;跟前端技术一样&#xff0c;就是为了美化界面。关键是&#xff0c;太好用了。之前还为此写过一篇博客。 Qt样式&#xff08;qss&#xff09;手册小结_大橘的博客-CSDN博客 其中主要是记录如何获取手册细节。 …

6、GPIO输入按键检测(轮询检测)

目录 0x01、简介 0x02、硬件设计 0x03、编写函数 0x001、按键初始化 0x002、按键检测 0x003、按键led翻转 0x04、源程序下载地址 0x01、简介 本次实验主要实现按键控制LED灯。 由于机械按键在按下和抬起的时候会产生按键抖动&#xff0c;所以在设计的时候需要考虑如何消除抖…

Pytorch可视化特征图(代码 亲测可用)

2013年Zeiler和Fergus发表的《Visualizing and Understanding Convolutional Networks》 早期LeCun 1998年的文章《Gradient-Based Learning Applied to Document Recognition》中的一张图也非常精彩&#xff0c;个人觉得比Zeiler 2013年的文章更能给人以启发。从下图的F6特征&…

会议OA项目-首页

目录一、Flex布局简介什么是flex布局&#xff1f;flex属性学习地址&#xff1a;案例演示二、轮播图组件及mockjs三、会议OA小程序首页布局一、Flex布局简介 布局的传统解决方案&#xff0c;基于盒状模型&#xff0c;依赖 display属性 position属性 float属性 什么是flex布局…

简单有效的Mac内存清理方法,不用收藏也能记住

Mac电脑使用的时间越久&#xff0c;系统的运行就会变的越卡顿&#xff0c;这是Mac os会出现的正常现象&#xff0c;卡顿的原因主要是系统缓存文件占用了较多的磁盘空间&#xff0c;或者Mac的内存空间已满。如果你的Mac运行速度变慢&#xff0c;很有可能是因为磁盘内存被过度占用…

如何理解并记忆DataFrame中的Axis参数

当我们遇到有axis参数的方法时&#xff0c;脑子里的第一反应应该是&#xff1a;这个方法一定是沿着某一方向进行某种“聚合”或者“过滤”操作。在此场景下&#xff0c;Axis参数就是用来设定操作方向的&#xff1a;是垂直方向还是水平方向&#xff1f; axis0: 一行一行推进&…

【微服务架构实战】第1篇之API网关概述

1.网关概述 采用分布式、微服务的架构模式开发系统时&#xff0c;API 网关是整个系统中必不可少的一环。 1.1 没有网关会有什么问题&#xff1f; 在微服务架构模式下&#xff0c;1个系统会被拆分成多个微服务&#xff0c;如果每个微服务都直接暴露给调用方&#xff0c;会有以…

MySQL主键和唯一键的区别

主键和唯一键基本知识参考这篇文章 MySQL表的约束 &#xff0c;本篇文章主要是谈一谈主键和唯一键的区别从而更好的理解唯一键和主键。 在上篇文章中已经提到 主键&#xff1a; primary key 用来唯一的约束该字段里面的数据&#xff0c;不能重复&#xff0c;不能为空&#x…

vue父页面调用子页面及方法及传参,鼠标光标定位

项目场景&#xff1a; vue父页面调用子页面及方法 问题描述 vue中父界面调用子界面及方法时界面可以调用&#xff0c;但是调用方法的时候第一次报错&#xff0c;但是关掉界面再次重新打开就没问题了 原因分析&#xff1a; 在我之前添加鼠标指针定位的时候&#xff0c;如果在…

记录scoped属性的使用和引发的问题

背景 在对表格数据进行样式处理时&#xff0c;通过业务逻辑判断&#xff0c;进行对符合要求的表格填充背景色&#xff0c;没有符合预期的效果。反复排查校验代码和判断逻辑&#xff0c;都没有什么问题&#xff0c;可能还是样式上出现问题。再通过F12 选取元素对表格设置背景色时…

获取树形结构中,父节点下所有子/孙节点(递归方式)

获取树形结构中&#xff0c;父节点下所有子/孙节点&#xff08;递归方式&#xff09;1 树形结构&#xff08;TreeItem类&#xff09;2 测试代码&#xff08;main函数&#xff09;3 运行效果1 树形结构&#xff08;TreeItem类&#xff09; 这里通用型树形结构为TreeItem类&…

初学Java web(七)RequestResponse

Request&Response Request:获取请求数据 Response:设置响应数据 一.Request对象 1.Request继承体系 Tomcat需要解析请求数据&#xff0c;封装为requestx对象并且创建requestx对象传递到service方法中 使用request对象&#xff0c;查阅JavaEE API文档的HttpServletReque…

rocketMq架构原理精华分析(一)

rocketMq架构原理精华分析是我们这篇文章的核心&#xff0c;从消息中间件的对比、架构模型、消息模型、常见问题等逐一分析&#xff1a; 一、中间件对比&#xff1a; RabbitMq 集群效果不太好&#xff0c;底层不是java 语言&#xff0c;研究原理比较困难&#xff1b; Kafka是…

前端面试题之计算机网络篇 OSI七层网络参考模型

互联网数据传输原理 &#xff5c;OSI七层网络参考模型 OSI七层网络参考模型 应用层&#xff1a;产生网络流量的程序表示层&#xff1a;传输之前是否进行加密或者压缩处理会话层&#xff1a;查看会话&#xff0c;查木马 netstat-n传输层&#xff1a;可靠传输、流量控制、不可…

亿级流量的互联网项目如何快速构建?手把手教你构建思路

一. 大流量的互联网项目 1.项目背景 索尔老师之前负责的一个项目&#xff0c;业务背景是这样的。城市的基础设施建设是每个城市和地区都会涉及到的&#xff0c;如何在基建工地中实现人性化管理&#xff0c;是当前项目的主要诉求。该项目要实现如下目标&#xff1a; 工地工人的…

C语言实现http下载器(附代码)

C语言实现http的下载器。 例&#xff1a;做OTA升级功能时&#xff0c;我们能直接拿到的往往只是升级包的链接&#xff0c;需要我们自己去下载&#xff0c;这时候就需要用到http下载器。 这里分享一个&#xff1a; 功能&#xff1a; 1、支持chunked方式传输的下载 2、被重定…