首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

Go 互斥锁 Mutex 源码分析(二)

编程知识
2024年08月24日 12:27

原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

Go 互斥锁 Mutex 源码分析(一) 一文中分析了互斥锁的结构和基本的抢占互斥锁的场景。在学习锁的过程中,看的不少文章是基于锁的状态解释的,个人经验来看,从锁的状态出发容易陷入细节,了解锁的状态转换过一段时间就忘,难以做到真正的理解。想来是用静态的方法分析动态的问题导致的。在实践中发现结合场景分析互斥锁对笔者来说更加清晰,因此有了 Go 互斥锁 Mutex 源码分析(一),本文接着结合不同场景分析互斥锁。

1. 不同场景下的锁状态

1.1 唤醒 goroutine

给出示意图:

image

G1 通过 Fast path 拿到锁,G2 在自旋之后,锁还是已锁状态。这是和 Go 互斥锁 Mutex 源码分析(一) 中的场景不一样的地方。接着自旋之后看,这种场景下会发生什么:

func (m *Mutex) lockSlow() {
	...
	for {
        if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
            ...
        }
        // step2: 当前锁未释放,old = 1
        new := old

        // step2: 如果当前锁是饥饿的,跳过期望状态 new 的更新
        // -      这里锁不是饥饿锁,new = old = 1
        if old&mutexStarving == 0 {
			new |= mutexLocked
		}

        // step2: 当前锁未释放,更新 new
        // -      更新 new 的等待 goroutine 位,表示有一个 goroutine 等待
        // -      更新 new 为 1001,new = 9 
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}

		// step2: 当前 goroutine 不是饥饿状态,跳过 new 更新
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}

        // step2: 当前 goroutine 不是唤醒状态,跳过 new 更新
        if awoke {
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			new &^= mutexWoken
		}

        // step3: 原子 CAS 更新锁的状态
        // -      这里更新锁 m.state = 1 为 m.state = new = 9
        // -      表示当前有一个 goroutine 在等待锁
        if atomic.CompareAndSwapInt32(&m.state, old, new) {
            ...
            // waitStartTime = 0, queueLifo = false
            queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
                // 更新 waitStartTime
				waitStartTime = runtime_nanotime()
			}

            // step4: 调用 runtime_SemacquireMutex 阻塞 goroutine
            runtime_SemacquireMutex(&m.sema, queueLifo, 1)
            starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
            ...
        }
    }
}

Mutex.lockSlow 中更新了锁状态,接着进入 runtime_SemacquireMutexruntime_SemacquireMutex 是个非常重要的函数,我们有必要介绍它。

runtime_SemacquireMutex 接收三个参数。其中,重点是信号量 &m.semaqueueLifo。如果 queueLifo = false,当前 goroutine 将被添加到等待锁队列的队尾,阻塞等待唤醒。

G2 执行到 runtime_SemacquireMutex 时将进入阻塞等待唤醒状态,那么怎么唤醒 G2 呢? 我们需要看解锁过程。

1.1.1 sync.Mutex.Unlock

在 G2 阻塞等待唤醒时,G1 开始释放锁。进入 sync.Mutex.Unlock

func (m *Mutex) Unlock() {
	...
	// 将 m.state 的锁标志位置为 0,表示锁已释放
	new := atomic.AddInt32(&m.state, -mutexLocked)
    // 检查 new 是否为 0,如果为 0 则表示当前无 goroutine 等待,直接退出
    // 这里 new = 9,G2 在等待唤醒
	if new != 0 {
		m.unlockSlow(new)
	}
}

进入 Mutex.unlockSlow

func (m *Mutex) unlockSlow(new int32) {
    // 检查锁是否已释放,释放一个已经释放的锁将报错
	if (new+mutexLocked)&mutexLocked == 0 {
		fatal("sync: unlock of unlocked mutex")
	}

    // 检查锁是普通锁还是饥饿锁
    if new&mutexStarving == 0 {
        // 这里 new = 8 是普通锁,进入处理普通锁逻辑
		old := new
		for {
            // 如果没有 goroutine 等待,则返回
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}

            // old 的唤醒位置 1,并且将等待的 goroutine 减 1,表示将唤醒一个等待中的 goroutine
            // 这里 new = 2
			new = (old - 1<<mutexWaiterShift) | mutexWoken
            // m.state = 8, old = 8, new = 2
            // CAS 更新 m.state = new = 2
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
                // 进入 runtime_Semrelease 唤醒 goroutine
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
        // 处理饥饿锁逻辑,暂略
		runtime_Semrelease(&m.sema, true, 1)
	}
}

sync.Mutex.Unlock 中的 runtime_Semrelease 唤醒队列中等待的 goroutine。其中,主要接收信号量 &m.semahandoff 两个参数。这里 handoff = false,将增加信号量,唤醒队列中等待的 goroutine G2。

1.1.2 唤醒 G2

唤醒之后,G2 继续执行后续代码:

func (m *Mutex) lockSlow() {
	...
	for {
		...
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			...
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)

			// 检查唤醒的 goroutine 是否是饥饿模式
			// 如果是饥饿模式,或等待锁时间超过 1ms 则将 goroutine 置为饥饿模式
			// 注意这是 goroutine 是饥饿的,不是锁是饥饿锁
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			
			// m.state 在 G1 unlock 时被更新为 2
			old = m.state

			// 锁不是饥饿锁,跳过
			if old&mutexStarving != 0 {
				...
			}
			awoke = true
			iter = 0
		}
	}
}

唤醒后的 G2 将 old 更新为 2。信号量增加,释放锁,只会唤醒一个 goroutine,被唤醒的 goroutine,这里是 G2,将继续循环:

func (m *Mutex) lockSlow() {
	...
	for {
		// old = 2,不会进入自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			...
		}
		// 更新 new:new 是期望 goroutine 更新的状态
		// 这里 new = old = 2
		new := old

		// old = 2,不是饥饿锁
		// 更新 new 为 011,3
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		// old = 2,表示锁已释放,不会将 goroutine 加入等待位
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		// 不饥饿,跳过
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		// awoke = true
		if awoke {
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			// 重置唤醒位,将 new 更新为 001,1
			new &^= mutexWoken
		}

		// m.state = 2, old = 2, new =1
		// CAS 更新 m.state= new = 1,表示当前 goroutine 已加锁
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			// 当前 goroutine 已加锁跳出循环
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			...
		}
	}
}

在循环一轮后,G2 将拿到锁,接着执行临界区代码,最后在释放锁。

这里的场景是唤醒之后,goroutine 不饥饿。那么饥饿锁又是如何触发的呢?我们继续看饥饿锁的场景。

1.2 饥饿锁

饥饿锁场景下的示意图如下:

image

当 G1 释放锁时,G3 正在自旋等待锁释放。当 G1 释放锁时,被唤醒的 G2 和自旋的 G3 竞争大概率会拿不到锁。Go 在 1.9 中引入互斥锁的 饥饿模式 来确保互斥锁的公平性。

对于互斥锁循环中的大部分流程,我们在前两个场景下也过了一遍,这里有重点的摘写,以防赘述。

首先,还是看 G2,当 G1 释放锁时,G2 被唤醒,执行后续代码。如下:

func (m *Mutex) lockSlow() {
	...
	for {
		...
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			...
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)

			// 唤醒 G2,G2 等待锁时间超过 1ms
			// starving = true
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs

			// 锁被 G3 抢占,m.state = 0011
			old = m.state

			// 这时候 old 还不是饥饿锁,跳过
			if old&mutexStarving != 0 {
				...
			}
			awoke = true
			iter = 0
		}
	}
}

唤醒 G2 之后,G2 等待锁时间超过 1ms 进入饥饿模式。接着进入下一轮循环:

func (m *Mutex) lockSlow() {
	...
	for {
		// old 是唤醒锁,不会进入自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			...
		}

		// 锁的期望状态,new = old = 0011
		new := old

		// 锁不是饥饿锁,更新 new 的锁标志位为已锁
		// new = 0011
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}

		// 锁如果是饥饿或者已锁状态更新 goroutine 等待位
		// new = 1011
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}

		// goroutine 饥饿,且锁已锁
		// 更新 new 为饥饿状态,new = 1111
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}

		// 这里 G2 是唤醒的,重置唤醒位
		// new = 1101
		if awoke {
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			new &^= mutexWoken
		}

		// CAS 更新 m.state = new = 1101
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			...
			// G2 入队列过,这里 queueLifo = true
			queueLifo := waitStartTime != 0

			// 将 G2 重新加入队列,并加入到队首,阻塞等待
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			...
		}
	}
}

G2 进入饥饿模式,将互斥锁置为饥饿模式,当前互斥锁状态为 m.state = 1101。G2 作为队列中的队头,阻塞等待锁释放。

类似的,我们看 G3 释放锁的过程。

1.2.1 释放饥饿锁

G3 开始释放锁:

func (m *Mutex) Unlock() {
	...

	// new = 1100
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		// 进入 Mutex.unlockSlow
		m.unlockSlow(new)
	}
}

func (m *Mutex) unlockSlow(new int32) {
	...
	// new = 1100,是饥饿锁
	if new&mutexStarving == 0 {
		...
	} else {
		// 进入处理饥饿锁逻辑
		// handoff = true,直接将队头阻塞的 goroutine 唤醒
		runtime_Semrelease(&m.sema, true, 1)
	}
}

1.2.2 饥饿锁唤醒

在一次的在队头中阻塞的 G2 被唤醒,接着执行唤醒后的代码:

func (m *Mutex) lockSlow() {
	...
	for {
		...
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			...
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state

			// old = 1100,是饥饿锁
			if old&mutexStarving != 0 {
				...

				// delta = -(1001)
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				if !starving || old>>mutexWaiterShift == 1 {
					...
					// delta = -(1101)
					delta -= mutexStarving
				}

				//更新互斥锁状态 m.state = 0001,退出循环
				atomic.AddInt32(&m.state, delta)
				break
			}
		}
	}
}

唤醒之后的 G2 直接获得锁,将互斥锁状态置为已锁,直到释放。

2. 锁状态流程

前面我们根据几个场景给出了互斥锁的状态转换过程,这里直接给出互斥锁的流程图如下:

image

3. 总结

本文是 Go 互斥锁 Mutex 源码分析的第二篇,进一步通过两个场景分析互斥锁的状态转换。互斥锁的状态转换如果陷入状态更新,很容易头晕,这里通过不同场景,逐步分析,整个状态,接着给出状态转换流程图,力图做到源码层面了解锁的状态转换。


From:https://www.cnblogs.com/xingzheanan/p/18377669
本文地址: http://www.shuzixingkong.net/article/1398
0评论
提交 加载更多评论
其他文章 线性dp:最长公共子串
最长公共子串 本文讲解的题与leetcode718.最长重复子数组,题意一模一样,阅读完本文以后可以去挑战这题。 力扣链接 题目叙述: 给定两个字符串,输出其最长公共子串的长度。 输入 ABACCB AACCAB 输出 3 解释 最长公共子串是ACC,其长度为3。 与最长公共子序列的区别 公共子串:
线性dp:最长公共子串
折腾 Quickwit,Rust 编写的分布式搜索引擎(专为从对象存储中实现亚秒级搜索而设计)
什么是 Quickwit? Quickwit 是首个能在云端存储上直接执行复杂的搜索与分析查询的引擎,并且具有亚秒级延迟。它借助 Rust 语言和分离计算与存储的架构设计,旨在实现资源高效利用、易于操作以及能够扩展到 PB 级数据量。 Quickwit 非常适合日志管理、分布式追踪以及通常为不可变数
折腾 Quickwit,Rust 编写的分布式搜索引擎(专为从对象存储中实现亚秒级搜索而设计) 折腾 Quickwit,Rust 编写的分布式搜索引擎(专为从对象存储中实现亚秒级搜索而设计)
从网友探秘 《黑神话:悟空》 的脚本说说C#
《黑神话:悟空》千呼万唤始出来。在正式发售后不到24小时,Steam在线玩家峰值突破222万,在Steam所有游戏在线玩家历史峰值中排名第二。第一拨玩家纷纷晒出好评,称这款现象级产品正式开启国产3A游戏(3A 俗称:大量的资源、大量的金钱和大量的时间)元年,黑神话悟空是国内首款3A游戏,画面剧情都很
从网友探秘 《黑神话:悟空》 的脚本说说C#
C++11新特性(二):语言特性
C++11新特性 语言特性 nullptr空指针 nullptr空指针的使用可以规避掉以往设置为NULL的风险。NULL在编译器中常常被设置为0或者其它数字,此时判断指针是否为NULL,即判断指针类型是否能够等于整型值,并不安全。 int *p = nullptr; 强类型枚举 强类型枚举不能隐式转
PG数据库导致断电/重启无法正常启动问题排查
PG数据库导致断电/重启无法正常启动问题排查 一、问题 数据库断电后,启动PG数据库后无法正常启动,报”psql: could not connect to server: No such file or directory”的错误,错误图片如下: 二、背景分析 数据库是单机版,使用k8s进行部署运
PG数据库导致断电/重启无法正常启动问题排查 PG数据库导致断电/重启无法正常启动问题排查 PG数据库导致断电/重启无法正常启动问题排查
【Azure Logic App】在逻辑应用中开启或关闭一个工作流是否会对其它工作流产生影响呢?
问题描述 使用标准版的Azure Logic App服务,可以创建多个工作流(workflow),如果在启用/禁用其它的工作流时,是否会对正在运行其它工作流造成影响呢? 问题解答 在实际的测验中,我们得到的答案是:会造成影响!在Disabled/Enabled同一个Logic App中的Workfl
【Azure Logic App】在逻辑应用中开启或关闭一个工作流是否会对其它工作流产生影响呢? 【Azure Logic App】在逻辑应用中开启或关闭一个工作流是否会对其它工作流产生影响呢? 【Azure Logic App】在逻辑应用中开启或关闭一个工作流是否会对其它工作流产生影响呢?
AD(Active Directory )域的搭建与操作
AD 域的搭建与操作 一、准备工作 准备好 VM 虚拟机和 Server 的安装包。 二、安装 Server 2022 选择标准且有图形界面的进行安装。 选择自定义安装方式。 为虚拟机 server2022 安装 VMware tools。 回到桌面,右键个性化把计算机和网络图标放出来。 三、安装
《Programming from the Ground Up》阅读笔记:p103-p116
《Programming from the Ground Up》学习第7天,p103-p116总结,总计14页。 一、技术总结 1.读写文件 (1)linux.s linux.s: #file name:linux.s # system call numbers(按数字大小排列,方便查看) .equ
《Programming from the Ground Up》阅读笔记:p103-p116