Mutex 互斥锁

概要描述

mutex 是 go 提供的同步原语。用于多个协程之间的同步协作。在大多数底层框架代码中都会用到这个锁。

mutex 总过有三个状态

  • mutexLocked: 表示占有锁
  • mutexWoken: 表示唤醒
  • mutexStarving: 表示等待锁的饥饿状态(从正常模式进入饥饿状态)

具体实现

首先得清楚 Mutex 的结构

type Mutex struct {
state int32
sema uint32
}

Lock 持有锁

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()
}

比较锁的状态,如果是初始状态 0,则获取当前锁并将锁的状态置为 mutexLocked 之后返回,即 1。其他协程则在释放锁之前走 m.lockSlow 代码分支。

m.lockSlow 代码很复杂,里面有一些比较的变量为了方便理解,我标记起来:

func (m *Mutex) lockSlow() {
var waitStartTime int64
starving := false
awoke := false
iter := 0
...
}
  • waitStartTime: 表示等待唤醒的时间,如果等待的时间超过了 1ms 则会走锁的饥饿模式(后面会讲到)
  • starving: 表示是饥饿标识
  • awoke: 是否被唤醒的标识
  • iter: 等待锁的次数

在申明这些变量之后,程序就会进入一个循环阶段,在这个循环体内,runtime 会根据当前锁的状态 m.state 以及等待锁的次数以及系统的自身运行情况来判断程序是否进入自旋等待。

// lockSlow
for {
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
...
}

上面的第一个判断语句就是判断了当前锁的状态是否为普通模式(只有普通模式才会进入自旋)。除此之外 runtime_canSpin 还会判断当前自旋的次数是否超过 4 次、当前操作系统是否为多核 cpu 、 GOMAXPROCS > 1 以及至少有一个正在运行的 P 并且 P 下的局部队列 runq 是空闲的。

在没有自旋的情况下,则正常往下执行,就会执行 runtime_SemacquireMutex 方法,目的就是获取当前锁的 m.sema 变量的地址作为信号量将当前的等待者进入 semaRoot 的等待优先级队列(链表结构)中。并且根据传的变量 queueLifo 来控制是传入到队列尾部还是头部。如果 queueLifo=true 说明之前已经处理等待状态,则就会插入到队列的头部等待唤醒,否则就进入队列的尾部。在这个 runtime_SemacquireMutex 方法中会不断尝试获取锁并进入休眠状态等待信号量释放。一旦获取锁立即返回执行后续的代码。

// lockSlow
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 old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
} //runtime_SemacquireMutex
func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) {
...
for {
lockWithRank(&root.lock, lockRankRoot)
// Add ourselves to nwait to disable "easy case" in semrelease.
atomic.Xadd(&root.nwait, 1)
// Check cansemacquire to avoid missed wakeup.
if cansemacquire(addr) {
atomic.Xadd(&root.nwait, -1)
unlock(&root.lock)
break
}
// Any semrelease after the cansemacquire knows we're waiting
// (we set nwait above), so go to sleep.
root.queue(addr, s, lifo)
goparkunlock(&root.lock, waitReasonSemacquire, traceEvGoBlockSync, 4+skipframes)
if s.ticket != 0 || cansemacquire(addr) {
break
}
}
if s.releasetime > 0 {
blockevent(s.releasetime-t0, 3+skipframes)
}
releaseSudog(s)
}

如上面代码所示,在插入队列之后就会根据标识 starving 以及 runtime_nanotime() - waitStartTime > 1ms 来判断当前等待锁模式是否进入饥饿模式。

在进入饥饿模式之前,等待者优先级队列中的第一个 g 被唤醒,如果其他新到达的 g 的优先级要比等待队列中的要高,所以这个等待着就会获取锁失败,那么就会转而进入到等待队列中的头部等待下次唤醒。而此时如果等待唤醒的队列等待时间超过了 1ms,那么就会进入饥饿模式,此时新到来的 g 就不会参与占有锁,也不会自旋,而是进入到等待着优先级队列中的队尾等待唤醒。

只有在这个等待队列为空,或者最后一个等待着的等待唤醒时间小于 1ms,那就会推出饥饿模式。

所以这个饥饿模式就是为了防止等待队列中的 g 陷入无限的等待状态。

如何根据 m.sema 获取等待者队列?

首先 rutime 会根据 m.sema 的地址通过哈希计算来生成一个 table,每个 mutex 的信号量地址对应一个表。每个表都有一个 semaRoot 的对象,这个对象包含一个 treap *sudog 链表结构队列。通过 semaroot.g = getg() 把当前的 g 绑定起来就进入到等待着队列了。

// mutex.go
func semroot(addr *uint32) *semaRoot {
return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root
}

Unlock 释放锁

知道了 Lock 的实现细节,那么释放锁我们就相对来说比较简单了。我们从源码的代码量就能知道要比 Lock 简单不少:

func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
m.unlockSlow(new)
}
}

首先就是通过原子操作将 m.state 减 1 操作让其回到初始状态 0。如果该锁还没有释放则进入 unlockSlow 操作

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 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 {
runtime_Semrelease(&m.sema, true, 1)
}
}

该操作仅仅做了当前锁状态是否为饥饿模式,如果不是就会正常唤醒等待队列中的 g,并通过 sema 信号量释放该信号量将所有权交给等待者。如果是饥饿模式,则直接将当前所有权交给队列中的下一个等待者。

总结

Mutex 在占有锁的时候会有两种模式,一种是正常模式,等待者会进入一个等待者队列等待被唤醒。其他 g 尝试获取锁时会根据系统自身情况以及是否有 runq 空闲的 P 来进入自旋。在自旋的过程中会计算锁的状态。由于正常模式下等待者队列中的 G 可能会无限等待下去,所以这个时候就会进入饥饿状态。会将锁的所有权直接交给被唤醒的等待者直到队列为空退出饥饿模式。

在进入等待者队列是会获取当前锁的信号量,这样保证了同一个时刻不会有多个 G 占有同一个锁。

解锁过程就是对应加锁过程的逆过程。

如果当前锁的状态不对,则直接抛异常;当互斥锁为饥饿模式时,直接将所有权交给等待者。当处于正常模式时,如果没有锁或者当前的锁状态不都为 0,则直接返回。否则就会释放信号量唤醒等待者队列中的等待者获取锁。

探索互斥锁 Mutex 实现原理的更多相关文章

  1. 深入理解Solaris内核中互斥锁(mutex)与条件变量(condvar)之协同工作原理

    在Solaris上写内核模块总是会用到互斥锁(mutex)与条件变量(condvar), 光阴荏苒日月如梭弹指一挥间,Solaris的大船说沉就要沉了,此刻心情不是太好(Orz).每次被年轻的有才华的 ...

  2. 互斥锁Mutex与信号量Semaphore的区别

    转自互斥锁Mutex与信号量Semaphore的区别 多线程编程中,常常会遇到这两个概念:Mutex和Semaphore,两者之间区别如下: 有人做过如下类比: Mutex是一把钥匙,一个人拿了就可进 ...

  3. Linux内核互斥锁--mutex

    一.定义: /linux/include/linux/mutex.h   二.作用及访问规则: 互斥锁主要用于实现内核中的互斥访问功能.内核互斥锁是在原子 API 之上实现的,但这对于内核用户是不可见 ...

  4. 线程锁(互斥锁Mutex)及递归锁

    一.线程锁(互斥锁) 在一个程序内,主进程可以启动很多个线程,这些线程都可以访问主进程的内存空间,在Python中虽然有了GIL,同一时间只有一个线程在运行,可是这些线程的调度都归系统,操作系统有自身 ...

  5. 线程锁(互斥锁Mutex)

    线程锁(互斥锁Mutex) 一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况? # -*- cod ...

  6. Golang 读写锁RWMutex 互斥锁Mutex 源码详解

    前言 Golang中有两种类型的锁,Mutex (互斥锁)和RWMutex(读写锁)对于这两种锁的使用这里就不多说了,本文主要侧重于从源码的角度分析这两种锁的具体实现. 引子问题 我一般喜欢带着问题去 ...

  7. 一文带你剖析LiteOS互斥锁Mutex源代码

    摘要:多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的临界资源,只能被独占使用.LiteOS使用互斥锁来避免这种冲突,互斥锁是一种特殊的二值性信号量,用于实现对临界资源的独占 ...

  8. 自旋锁Spin lock与互斥锁Mutex的区别

    POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套常用的API.线程同步(Thread Synchronization)是并行编程中非常重要的通讯手段,其中最典型的应用 ...

  9. Linux 互斥锁的实现原理(pthread_mutex_t)

    本文参考--http://www.bitscn.com/os/linux/201608/725217.html 和http://blog.csdn.net/jianchaolv/article/det ...

随机推荐

  1. TODO不实现会报错

    kotlin.NotImplementedError: An operation is not implemented: Not yet implemented 会发生--当你 override fu ...

  2. Go语言设计模式之函数式选项模式

    Go语言设计模式之函数式选项模式 本文主要介绍了Go语言中函数式选项模式及该设计模式在实际编程中的应用. 为什么需要函数式选项模式? 最近看go-micro/options.go源码的时候,发现了一段 ...

  3. 获取两个时间点间的随机时间&时间戳

    获取两个时间点间的随机时间&时间戳 方案一 # python2 不兼容,python3正常 import datetime,random def randomtimes(start, end, ...

  4. 慢镜头变焦:视频超分辨率:CVPR2020论文解析

    慢镜头变焦:视频超分辨率:CVPR2020论文解析 Zooming Slow-Mo:  Fast and Accurate One-Stage Space-Time Video Super-Resol ...

  5. KITTI数据集上MaskRCNN检测效果示例

    KITTI数据集上MaskRCNN检测效果示例 在Semantic Instance Segmentation Evaluation中,MaskRCNN性能效果排名第一. Test Image 0 I ...

  6. VB 老旧版本维护系列---尴尬的webapi访问返回json对象

    尴尬的webapi访问返回json对象 首先Imports Newtonsoft.Json Imports MSXML2(Interop.MSXML2.dll) Dim URLEncode As Sy ...

  7. C#搞跨平台桌面UI,分别实现Windows,Mac,Linux屏幕截图

    搞跨平台IM,截图功能少不了. Windows 创建GDI的兼容位图,把桌面的图像通过BitBlt拷贝到兼容位图里,通过兼容位图的数据指针创建Bitmap对象,由于兼容位图的内存是非托管的,Bitma ...

  8. 09:CBV与settings

    CBV源码 # 切入点 url(r'^login/', views.Mylogin.as_view()) '''类名点名字还加括号 名字要么是绑定给类的方法 要么是无参函数''' ​ 1.as_vie ...

  9. 基于 Spring Security 的前后端分离的权限控制系统

    话不多说,入正题.一个简单的权限控制系统需要考虑的问题如下: 权限如何加载 权限匹配规则 登录 1.  引入maven依赖 1 <?xml version="1.0" enc ...

  10. 带你从头到尾捋一遍MySQL索引结构

    索性这次把数据库中最核心的也是最难搞懂的内容,也就是索引,分享给大家. 这篇博客我会谈谈对于索引结构我自己的看法,以及分享如何从零开始一层一层向上最终理解索引结构. 从一个简单的表开始 create ...