Go-知识并发控制mutex

news2024/11/24 16:46:57

Go-知识并发控制mutex

  • 1. 介绍
  • 2. 数据结构
    • 2.1 Mutex 结构体
    • 2.2 Mutex 方法
  • 3. 加锁解锁过程
    • 3.1 简单加锁
    • 3.2 加锁被阻塞
    • 3.3 简单解锁
    • 3.4 解锁并唤醒协程
  • 4. 自旋过程
    • 4.1 什么是自旋
    • 4.2 自旋条件
    • 4.3 自旋的优势
    • 4.4 自旋的问题
  • 5. Mutex 模式
    • 5.1 Normal 模式
    • 5.2 Starving 模式(饥饿模式)
  • 6. Woken 状态
  • 7. 重复解锁引发 panic
  • 8. 总结

gitio: https://a18792721831.github.io/

1. 介绍

互斥锁是并发程序中对共享资源进行访问控制的主要手段,Go 语言提供了非常简单易用的 Mutex。
Mutex 是结构体类型,对外暴露了 Lock 和 Unlock 两个方法,用于加锁和解锁。

2. 数据结构

2.1 Mutex 结构体

在源码包 src/sync/mutex.go中定义了互斥锁的数据结构:
在这里插入图片描述

Mutex.state 表示互斥锁的状态,比如是否被锁定等等
Mutex.sema 表示信号量,协程阻塞等待该信号量,解锁的写成释放信号量,从而唤醒等待信号量的协程

其中 Mutex.state 是 32 位的整型变量,内部实现时,把该变量分成四部分,用于记录 Mutex 的四种状态。
在这里插入图片描述

  • Locked: 表示该 Mutex 是否已被锁定,0表示没有锁定,1表示已被锁定.
  • Woken: 表示是否有协程已被唤醒,0表示没有协程唤醒,1表示已有协程唤醒,正在加锁过程中.
  • Starving: 表示该 Mutex 是否处于饥饿状态,0表示没有饥饿,1表示饥饿,说明有协程阻塞超过了1ms
  • Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。

协程之间的抢锁实际上是抢给 Locked 赋值的权利,能给 Locked 赋值 1,就说明抢锁成功。
抢不到就阻塞等待 Mutex.sema 信号量,一旦持有锁的协程解锁,等待的协程就会依次被唤醒。

2.2 Mutex 方法

Mutex 对外提供的方法主要是加锁和解锁:

  • Lock(): 加锁
  • Unlock(): 解锁
  • TryLock(): 非阻塞的方式尝试加锁(Go 1.18 引入)

3. 加锁解锁过程

3.1 简单加锁

假设当前只有一个协程在加锁,没有其他协程干扰:
在这里插入图片描述

加锁过程中会判断 Locked 的标志位是否为0,如果是0则把 Locked 位置为1,代表大锁成功。
如上图所示,加锁成功后,只有 Locked 位置为1,其他位置都没变。

func (m *Mutex) Lock() {
	// 快速尝试,使用 cas 进行加锁,如果 state 等于 0 表示无锁
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		return
	}
	// 否则就需要慢加锁了
	m.lockSlow()
}
func (m *Mutex) lockSlow() {
    // 等待开始时间
	var waitStartime int64
	// 是否饥饿
	starving := false
	// 是否有协程被唤醒
	awoke := false
	iter := 0
	// 获取 state 值
	old := m.state
	// 死循环
	for {
		// old & mutexLocked 表示取出 Locked 位
		// old & mutexStarving 表示取出 Starving 位
		// old & (mutexLocked | mutexStarving) 表示取出 Locked 或 starving 位
		// 所以 old & (mutexLocked|mutexStarving) == mutexLocked 表示 Locked 或 Starving 等于 1。
		// 要么有锁,要么饥饿
		// 并且当前 goroutine 可以进行自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			// 没有饥饿 而且 没有协程被唤醒 并且 Waiter 等待的协程不为空
			// 尝试使用 CAS 设置 Worken 值,表示有 goroutine 被唤醒了,在自旋等待锁
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				// 是否有协程被唤醒标志置为 true 
				awoke = true
			}
			// 自旋 ,我认为可以理解为 continue ..
			runtime_doSpin()
			// 自旋次数加1
			iter++
			// 重新获取最新的 state 值
			old = m.state
			// 自旋
			continue
		}
		// 重新获取 state
		new := old
		// 没有饥饿
		if old&mutexStarving == 0 {
		    // 新值设置为加锁了
			new |= mutexLocked
		}
		// 如果存在饥饿或者有锁
		if old&(mutexLocked|mutexStarving) != 0 {
		    // Waiter 数量增加 1 
		    // 1<<mutexWaiterShift 表示将1左移3位,1000
		    // new += 1000 表示将 Waiter + 1
			new += 1 << mutexWaiterShift
		}
		// 饥饿 并且 有锁
		if starving && old&mutexLocked != 0 {
		    // new 设置 Starving 位为1
			new |= mutexStarving
		}
		// 如果有唤醒的协程
		if awoke {
			// 如果新值的 Woken 为 0 
			if new&mutexWoken == 0 {
			    // 当前协程就是唤醒的协程
				throw("sync: inconsistent mutex state")
			}
			// 异或运算 将 Woken 位清除
			new &^= mutexWoken
		}
		// 设置有锁,设置没有唤醒的协程,保留饥饿,Waiter + 1
		// 放弃自旋,放弃唤醒,进入阻塞并等待
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
		    // 如果既没有锁,而且也没有饥饿,表示拿到了锁
			if old&(mutexLocked|mutexStarving) == 0 {
			    // 自旋获得了锁
				break // 用CAS锁定了互斥对象
			}
			// 等待时间不为0,表示之前就在等待中
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
			    // 如果等待时间为0,表示第一次等待
				waitStartTime = runtime_nanotime()
			}
			// 根据等待时间,将当前 goroutine 放到队列头或者尾
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			// 设置是否饥饿
			// 如果原来就饥饿,那么依然饥饿
			// 或者等待时间超过限制,那么也是饥饿
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			// 重新获取 state 
			old = m.state
			// 如果存在饥饿
			if old&mutexStarving != 0 {
			    // 有锁 或 有唤醒的协程 或者
			    // 没有等待的协程
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
				    // 不能即处于饥饿,有存在唤醒,还没有锁
				    // 不能没有等待的协程,又处于饥饿状态
					throw("sync: inconsistent mutex state")
				}
				// 表示 1 减去 1000 = -7 
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				// 如果不是饥饿模式 或者 Waiter 等于 1
				if !starving || old>>mutexWaiterShift == 1 {
					// -7 减去 -4 ,移除饥饿模式
					delta -= mutexStarving
				}
				// 更新 state
				// 防止自旋死锁
				atomic.AddInt32(&m.state, delta)
				break
			}
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}
}

3.2 加锁被阻塞

假设加锁时,锁已经被其他协程占用了:
在这里插入图片描述

当协程B对一个已被占用的锁再次加锁时,Waiter 计数器增加1,此时协程B被阻塞,直到 Locked 值变为0后才会被唤醒。

3.3 简单解锁

假设解锁时,没有其他协程阻塞:
在这里插入图片描述

由于没有其他协程阻塞等待加锁,所以此时解锁时只需要把 Locked 位置为0即可,不需要释放信号量。

func (m *Mutex) Unlock() {
	// 使用 CAS 快速解锁
	new := atomic.AddInt32(&m.state, -mutexLocked)
	// 如果 CAS 解锁失败,那么尝试慢解锁
	if new != 0 {
		m.unlockSlow(new)
	}
}
func (m *Mutex) unlockSlow(new int32) {
    // 如果 无锁 new 最低位 1,因为在 Unlock 中已经减去 1 了 ,1+1 = 10 , 0 & 1 = 0 , 0 == 0 
    // 如果 有锁 new 最低位 0,0+1 = 1, 1 & 1 = 1, 1 == 0
    // 所以,无锁异常,有锁继续 
	if (new+mutexLocked)&mutexLocked == 0 {
		throw("sync: unlock of unlocked mutex")
	}
	// 如果没有饥饿
	if new&mutexStarving == 0 {
	    // 临时赋值
		old := new
		for {
		    // Waiter 等于 0 ,没有等待的协程 或
		    // 处于 饥饿,有锁,有唤醒 ,结束
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// Waiter 减 1,并且 保留唤醒状态
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			// 尝试使用 CAS 将 Waiter 减 1 ,同时设置无锁
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
			    // 释放 1 个信号量
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
		// 如果处理饥饿模式,直接唤醒协程,释放信号量
		runtime_Semrelease(&m.sema, true, 1)
	}
}

3.4 解锁并唤醒协程

假设解锁时,有一个或多个协程阻塞:
在这里插入图片描述

协程A解锁过程分为两个步骤,一是把 Locked 位置为0,二是看到 Waiter > 0 ,释放1个信号量,唤醒一个阻塞的协程,被唤醒的协程 B 把 Locked 位置为1,于是协程B获得锁。

4. 自旋过程

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

4.1 什么是自旋

自旋对应于CPU的PAUSE指令,CPU对该指令什么都不做,相当于CPU空转,对程序而言相当于 sleep 了一小段时间,时间非常短,当前实现是 30个时钟周期。
自旋过程中会持续探测 Locked 是否变为0 ,连续两次探测奸恶就是在执行这些 PAUSE 指令,不同于sleep,不需要将协程转为睡眠状态。

4.2 自旋条件

加锁时,程序会自动判断是否可以自旋,无限制的自旋会给CPU带来巨大压力,所以判断是否可以自旋就很重要了。
自旋必须满足一下所有条件:

  • 自旋次数要足够小,通常为 4 ,即自旋最多4次
  • CPU核数大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
  • 协程调度机制中的 Process 数量要大于1,比如使用 GOMAXPROCESS() 将处理器设置为1就不能启动自旋
  • 协程调度机制中的可运行队列必须为空,否则会延迟协程调度。

简单来说就是 不忙 的时候才会启动自旋

4.3 自旋的优势

自旋的优势是更充分地利用CPU,尽量避免协程切换。因为当前申请加锁的协程拥有CPU,如果经过短时间的自旋可以获得锁,则蒋倩协程可以继续运行,不必进入阻塞状态。

4.4 自旋的问题

如果自旋过程中获得锁,那么之前被阻塞的协程将无法获得锁,如果加锁的协程特别多,每次都通过自旋获得锁,那么之前被阻塞的协程将很难获得锁,从而进入 Starving 状态。
为了避免协程长时间无法获取锁,自 1.8 版本依赖增加了一个状态, Starving 。在这个状态下不会自旋,一旦有协程释放锁,那么一定会唤醒一个协程并成功加锁。

5. Mutex 模式

每个 Mutex 都有两个模式,Normal 和 Starving 。

5.1 Normal 模式

默认情况下,Mutex 的模式是 Normal。
在Normal模式下,如果协程加锁不成功不会立即转为阻塞排队,而是判断是否满足自旋的条件,如果满足则会启动自旋,尝试抢锁。

5.2 Starving 模式(饥饿模式)

自旋过程中能抢到锁,一定意味着同一时间有协程释放了锁。释放锁时,如果发现有阻塞等待的协程,那么还会释放一个信号量来唤醒一个等待协程,
被唤醒的协程得到CPU后开始运行,此时发现锁已经被抢占了,只能再次阻塞。阻塞钱会判断自上次阻塞到本次阻塞经过了多长时间,如果超过1ms,则会将Mutex 标记为 Starving 模式,然后阻塞。
在 Starving 模式下,不会启动自旋过程,即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将乘公共获取锁,同时会把等待计数减1.

6. Woken 状态

Woken 状态用与加锁和解锁过程的通信。
比如同一个时刻,两个协程一个在加锁,另一个在解锁,加锁的协程可能在自旋过程中,此时把 Woken 标记为1,用于通知解锁协程不必释放信号量。
存在唤醒中的协程,不需要信号量唤醒。

7. 重复解锁引发 panic

如果 Mutex 处于无锁状态,执行 Unlock 会触发 panic。
Unlock 的逻辑分为两个过程: 将 Locked 置为 0 ;判断 Waiter 数量,Waiter > 0,释放一个信号量。
如果多次执行Unlock,那么可能每次都释放一个信号量,这样会唤醒多个协程,多个协程被唤醒后继续在 Lock 的逻辑中抢锁,就需要在 Lock 的逻辑中增加更多场景,
并且引发更多的协程切换。

8. 总结

在使用 Mutex 的时候,使用 defer 避免死锁。加锁后立即使用 defer 解锁,可以有效避免死锁。
加锁和解锁应该成对出现,最好是出现在同一个层次的代码块中,否则很容易引发因重复解锁导致的panic 。

(Java允许重复 release)

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

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

相关文章

MUR20100DC-ASEMI智能AI应用MUR20100DC

编辑&#xff1a;ll MUR20100DC-ASEMI智能AI应用MUR20100DC 型号&#xff1a;MUR20100DC 品牌&#xff1a;ASEMI 封装&#xff1a;TO-263 恢复时间&#xff1a;35ns 最大平均正向电流&#xff08;IF&#xff09;&#xff1a;20A 最大循环峰值反向电压&#xff08;VRRM&a…

设计软件有哪些?粒子插件篇,渲染100邀请码1a12

设计师常常需要设计特效&#xff0c;而粒子系统是必不可少的&#xff0c;这次我们简单介绍一些粒子插件。 1、ComplexFresnel ComplexFresnel插件是一款用于计算机图形渲染中的增强型菲涅尔效应模拟工具。它扩展了传统的菲涅尔效应模型&#xff0c;考虑了更多的光学参数&…

嵌入式PID算法总结

参考 CMSIS-DSP PID 控制 学习历程 最开始&#xff0c;根据公式自己写PID算法&#xff1b;后面找资料时&#xff0c;发现wiki上介绍PID时&#xff0c;提供了伪代码&#xff0c;直接照着翻译一下就可用了&#xff1b;然后想要实现一个自己的PID库&#xff08;能够实现多级PID…

小程序大作为|小程序开发详细流程,新手也能轻松掌握

随着移动互联网的快速发展&#xff0c;小程序作为一种轻量级应用&#xff0c;因其无需下载安装、即点即用、用完即走的特点&#xff0c;受到了广大用户的青睐。那么开发小程序都有哪些开发流程呢&#xff1f;可以用哪种方式开发&#xff1f;选择合适的开发方式&#xff0c;一起…

网络与协议安全复习 - 电子邮件安全

文章目录 PGP(Pretty Good Privacy)功能 S/MIME(Secure/Multipurpose Internet Mail Extensions)DKIM(Domain Keys Identified Mail) PGP(Pretty Good Privacy) 使用符号&#xff1a; Ks&#xff1a;会话密钥、KRa&#xff1a;A 的私钥、KUa&#xff1a;A 的公钥、EP&#xff…

【全开源】沃德会务会议管理系统(FastAdmin+ThinkPHP+Uniapp)

沃德会务会议管理系统一款基于FastAdminThinkPHPUniapp开发的会议管理系统&#xff0c;对会议流程、开支、数量、标准、供应商提供一种标准化的管理方法。以达到量化成本节约&#xff0c;风险缓解和服务质量提升的目的。适用于大型论坛、峰会、学术会议、政府大会、合作伙伴大会…

IT运维服务方案

在现代企业中&#xff0c;信息系统已经成为业务运营的核心。为了确保这些系统的高效、稳定运行&#xff0c;制定一套完善的IT运维服务方案至关重要。本文将探讨如何设计和实施一个高效的IT运维服务方案&#xff0c;确保信息系统的持续健康运行。 IT运维服务的目标 IT运维服务…

LED显示屏的对比度:揭开画面层次的神秘面纱

随着商业显示技术的飞速发展&#xff0c;LED显示屏已成为广告、会议、展览等领域不可或缺的视觉媒介。在诸多LED显示屏参数中&#xff0c;“对比度”这一指标日益受到用户和行业的关注。那么&#xff0c;究竟什么是对比度&#xff1f;它为何如此重要&#xff1f;下面将用通俗易…

气膜展厅:创新展览空间的新趋势—轻空间

随着科技的发展和市场需求的多样化&#xff0c;气膜展厅作为一种创新的展览空间&#xff0c;正在受到越来越多行业的青睐。其独特的结构和灵活的应用使得展览活动更加丰富多彩。 高效灵活的展览场地 气膜展厅最大的优势在于其便捷的搭建和拆卸。传统展览场地往往需要长时间的准…

哪些医疗器械需要注意网络安全问题?医疗器械网络安全测试方法有哪些?

随着医疗设备的网络化程度不断提高&#xff0c;网络安全问题变得越来越突出。以下是一些需要特别注意网络安全的医疗器械类别&#xff1a;1. 医学影像类设备&#xff1a;包括CT、DR、医用X射线系统、超声诊断仪、磁共振设备、肿瘤治疗机、医用胶片及处理系统、医用3D打印设备等…

易快报与E签宝完美对接,助力企业实现高效文件管理

一、客户介绍 某科技有限公司是一家专注于冷链物流领域的高新技术企业。公司凭借先进的物流技术和丰富的行业经验&#xff0c;为客户提供全方位的冷链物流服务。该公司致力于通过科技手段优化物流流程&#xff0c;确保货物在运输过程中的温度控制和品质安全。 二、客户痛点 该…

MySQL小版本升级(8.0.36->8.0.37)

关于MySQL升级的话MySQL官方文档上面介绍了2个方法&#xff0c;’就地升级’和‘逻辑升级’。’就地升级‘就是升级底层的RPM包而‘逻辑升级’就是将旧MySQL数据库上的信息迁移到新MySQL数据库上。 本篇文章介绍到的是RPM包升级 升级MySQL版本的典型步骤包括&#xff1a; 备份&…

超级会员卡积分收银系统源码 带完整的安装代码包以及搭建部署教程

系统概述 超级会员卡积分收银系统源码是一款专为商业运营打造的综合性软件解决方案。它集成了会员卡管理、积分管理、收银管理等多种功能&#xff0c;旨在为企业提供高效、便捷、准确的运营管理工具。 该系统源码采用先进的技术架构&#xff0c;具有良好的稳定性和扩展性&…

从WebM到MP3:利用Python和wxPython提取音乐的魔法

前言 有没有遇到过这样的问题&#xff1a;你有一个包含多首歌曲的WebM视频文件&#xff0c;但你只想提取其中的每一首歌曲&#xff0c;并将它们保存为单独的MP3文件&#xff1f;这听起来可能有些复杂&#xff0c;但借助Python和几个强大的库&#xff0c;这个任务变得异常简单。…

逆向海淘商业模式案例分析:Cssbuy淘宝代购集运系统丨淘宝代购集运系统搭建攻略

逆向海淘商业模式案例分析&#xff1a;Cssbuy淘宝代购集运系统 一、系统概述 Cssbuy淘宝代购集运系统是一个专为海外消费者设计的跨境淘宝代购集运平台。该系统通过整合中国电商平台的商品资源&#xff0c;为海外消费者提供一站式的购物及物流解决方案。其特点主要体现在以下几…

springboot知识点大全

文章目录 LombokLombok介绍Lombok常用注解Lombok应用实例代码实现idea安装lombok插件 Spring InitializrSpring Initializr介绍Spring Initializr使用演示需求说明方式1: IDEA创建方式2: start.spring.io创建 注意事项和说明 yaml语法yaml介绍使用文档yaml基本语法数据类型字面…

无线领夹麦克风哪款好,领夹麦克风哪个品牌好,多款麦克风推荐

​科技发展让无线领夹麦克风成为现代演讲、演出和采访不可或缺的工具。这种小巧便携的设备让我们摆脱线缆束缚&#xff0c;自由移动同时保持声音清晰稳定。无线领夹麦克风怎么选呢&#xff1f;接下来&#xff0c;我们介绍几款市面上综合表现相当不错的无线领夹麦克风给大家来参…

整合微信支付一篇就够了

需要的工具 微信开发小程序工具 需要的材料 关键步骤 postman获取微信access_token https://api.weixin.qq.com/cgi-bin/token?appid=wxfssafa629021&grant_type=client_credential&secret=701d213dsfsdfsfdss4fb274生成h5跳转小程序的链接 https://api.weixin.…

内容安全复习 3 - 深度学习基础

文章目录 深度学习概述神经网络简介损失函数反向传播 卷积神经网络什么是卷积神经网络卷积最大池化展平典型的神经网络结构 Transformer&#xff08;转换器、变压器&#xff1f;&#xff09;自注意力机制多头注意力机制 深度学习概述 前文提到深度学习分三步&#xff1a;神经网…

express+vue在线im实现【四】

往期内容 expressvue在线im实现【一】 expressvue在线im实现【二】 expressvue在线im实现【三】 本期示例 本期总结 支持了音频的录制和发送&#xff0c;如果觉得对你有用&#xff0c;还请点个免费的收藏与关注 下期安排 在线语音 具体实现 <template><kl-dial…