前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >go锁mutex与RWMutex

go锁mutex与RWMutex

作者头像
leobhao
发布于 2024-12-26 01:37:38
发布于 2024-12-26 01:37:38
940
举报
文章被收录于专栏:涓流涓流

sync.Mutex

sync.Mutex 是 go 最基本的同步原语, 也是最常用的锁之一

基本结构
代码语言:txt
AI代码解释
复制
// sync/mutex.go 25行
type Mutex struct {
	state int32
	sema  uint32
}
  • state: 当前互斥锁的状态
  • sema: 控制锁状态信号量

state 一共32位, 最低三位分别表示 mutexLockedmutexWokenmutexStarving,剩下的位置用来表示当前有多少个 Goroutine 在等待互斥锁的释放:

  • mutexLocked: 第 0 位, 是否上锁
  • mutexWoken: 第 1 位, 是否有协程抢占锁
  • mutexStarving: 第 2 位, 是否处于饥饿模式
  • 后续的高 29 位表示阻塞队列中等待的协程数量
加锁/解锁方案

最简单的思路去实现 mutex 互斥锁:

  • 加锁:把锁状态 0 改为 1, 假若已经是 1,则上锁失败,需要等他人解锁
  • 解锁:把 1 置为 0.

针对 goroutine 加锁时发现锁已被抢占的这种情形,此时摆在面前的策略有如下两种:

  • 阻塞/唤醒:将当前 goroutine 阻塞挂起,直到锁被释放后,以回调的方式将阻塞 goroutine 重新唤醒,进行锁争夺;
  • 自旋 + CAS:基于自旋结合 CAS 的方式,反复尝试试图获取锁

方案

优势

劣势

使用场景

阻塞/唤醒

精准分配,不浪费 CPU 时间片

需要挂起协程,进行上下文切换,操作较重

并发竞争激烈的场景

自旋+CAS

无需阻塞协程,短期来看操作较轻

长时间争而不得,会浪费 CPU 时间片

并发竞争强度低的场景

可以看到上面两种方式各有优劣, 基本上其他语言的锁也是基于这两个模式 ,如 java 的 synchronize 也是从偏向锁升级到轻量级锁->重量级锁

类似, Mutex 也有一个锁升级的过程:

  1. 首先保持乐观,goroutine 采用自旋 + CAS 的策略争夺锁;
  2. 尝试持续受挫达到一定条件后,判定当前过于激烈,则由自旋转为 阻塞/挂起模式.

从文档来看, 这里的升级条件是:

  1. 自旋累计达到 4 次仍未取得锁;
  2. CPU 单核或仅有单个 P 调度器;(此时自旋,其他 goroutine 根本没机会释放锁,自旋纯属空转);
  3. 当前 P 的执行队列中仍有待执行的 G. (避免因自旋影响到 GMP 调度效率)
正常模式和饥饿模式

Mutex 有两种模式:

  • 正常模式: 在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁(新的 goroutine 已经在 cpu 上运行了, 大概率会获取到时间片),为了减少这种情况的出现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine 被饿死。
  • 饥饿模式: 互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式
加锁/解锁源码

首先看加锁过程:

代码语言:txt
AI代码解释
复制
// sync/mutex.go 72 行
func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	m.lockSlow()
}

// slow path
func (m *Mutex) lockSlow() {
	var waitStartTime int64
	starving := false
	awoke := false
	iter := 0
	old := m.state
	for {
	    
		// 是否允许自旋
		// 如果当前锁处于已被锁定(mutexLocked)&& 未处于饥饿(mutexStarving)状态 && 当前允许自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
		    // 在自旋过程中,如果当前 goroutine 还没被唤醒(awoke == false)
		    // 并且互斥锁的唤醒标记位没被设置(old&mutexWoken == 0)且存在等待者(old>>mutexWaiterShift!= 0,通过位移操作判断等待者数量)
		    // 尝试将唤醒标记位 mutexWoken 设置上(避免其他 goroutine 被唤醒来和当前 goroutine 抢占锁)
			// 如果设置成功,就将 awoke 标记为 true
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				awoke = true
			}
			// 调用 runtime_doSpin 函数进行实际的自旋操作
			runtime_doSpin()
			// 更新自旋次数, 重新获取锁的当前状态old,继续下一轮循环尝试自旋获取锁
			iter++
			old = m.state
			continue
		}
		
		// 先记录原来的 state 值 old, 看下面的情况来进行更新
		new := old
		
		// 如果当前不处于 mutexStarving(饥饿), 给 state 加上 mutexLocked 锁定标记
		// 表示当前 goroutines 想获取锁(如果下面更新 state 成功就代表获取锁成功)
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
		
		// 如果当前已经处于锁定或饥饿状态(已经有 goroutine 持有锁或处于竞争激烈的饥饿态)
		// 则将 waiter(等待数量) +1(左移操作)
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
        
        // 如果当前 goroutine 处于饥饿模式, 并且当前状态是锁定
        // 则更新状态加上饥饿标识
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
		
		// 如果是当前 goroutine 设置了唤醒标志位
		// 首先对 state 进行状态校验, 没有设置唤醒标志位就抛出异常
		// 然后清除 mutexWoken 标志位(当前 goroutine 继续执行的话要么抢占锁要么被挂起, 所以需要 woken 抢占标识重置)
		if awoke {
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			new &^= mutexWoken
		}
		
		
		// 更新 state 标志位
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
		    // 如果原来的状态是锁定并且非饥饿状态, 
		    // 代表当前 goroutine 拿到了锁(加锁标志位是当前的 goroutine 更新的, 也就是获取到了锁), 直接 break 跳出循环
			if old&(mutexLocked|mutexStarving) == 0 {
				break // locked the mutex with CAS
			}
			
			// 否则不是当前 goroutine 加锁成功, 则进入阻塞流程
			
			// queueLifo 标识当前 goroutine 是从阻塞队列唤醒的还是新加入竞争的
			// 根据 waitStartTime 是否为 0 来确定,如果之前已经开始等待了,waitStartTime不为 0,就按 LIFO 排队;
			// 如果是刚开始等待,就记录当前时间作为 waitStartTime
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			// runtime_SemacquireMutex 阻塞等待获取锁
			// 加入阻塞,如果是 lifo(是被唤醒的 goroutine )就插入表头。 等待被信号量唤醒
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			
			
			// 走到这里代表 goroutine 从阻塞队列被唤醒了
			
			// 判断是否进入饥饿状态,
			// 通过比较当前时间(runtime_nanotime)减去开始等待时间(waitStartTime)是否超过了设定的饥饿阈值(starvationThresholdNs),
			// 如果超过了,就将 starving 标记为 true
			// 这里的 || 表达式用的比较巧妙, 如果 starving 已经是 true 了就不会去比较表达式了
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			
			// 再次把当前的状态赋值给 old
			old = m.state
			// 如果锁处于饥饿状态,需要做一些状态修正操作
			if old&mutexStarving != 0 {
			    // 先检查状态是否一致
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				// 然后计算一个状态差值delta,在满足一定条件(比如不是持续饥饿或者只有一个等待者等)时,
				// 会调整锁的状态来退出饥饿模式,然后 break 跳出循环
				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
		}
	}

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

解锁流程:

代码语言:txt
AI代码解释
复制
func (m *Mutex) Unlock() {
    // 解锁
    new := atomic.AddInt32(&m.state, -mutexLocked)
    // 解锁完成如果有其他阻塞 goroutine, 进入 unlockSlow, 没有就直接返回
    if new != 0 {
        m.unlockSlow(new)
    }
}

func (m *Mutex) unlockSlow(new int32) {
    // 状态检查
    // new+mutexLocked 如果原来锁定标志位是0, 然后与 mutexLocked 与操作就进位了
    // 所以这里是检测锁标志
	if (new+mutexLocked)&mutexLocked == 0 {
		fatal("sync: unlock of unlocked mutex")
	}
	
	// 如果没有处于饥饿模式
	if new&mutexStarving == 0 {
		old := new
		for {
		    // 如果阻塞队列内无 goroutine 
		    // 或者 mutexLocked、mutexStarving、mutexWoken 标识位任一不为零(三个状态均说明此时有其他活跃协程已介入)
		    // 上述两种情况直接 return 自身无需关心后续流程
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			
			
			// 将 state 中阻塞协程状态减一, 然后唤醒队列头的 goroutine
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
				runtime_Semrelease(&m.sema, false, 1)
				return
			}
			old = m.state
		}
	} else {
	    // 如果处于饥饿模式。 直接唤醒阻塞队列头部的 goroutine
		runtime_Semrelease(&m.sema, true, 1)
	}
}

sync.RWMutex

可以把 RWMutex 理解为一把读锁加一把写锁;

  • 写锁具有严格的排他性,当其被占用,其他试图取写锁或者读锁的 goroutine 均阻塞;
  • 读锁具有有限的共享性,当其被占用,试图取写锁的 goroutine 会阻塞,试图取读锁的 goroutine 可与当前 goroutine 共享读锁;

综上,RWMutex 适用于读多写少的场景,最理想化的情况,当所有操作均使用读锁,则可实现去无化;最悲观的情况,倘若所有操作均使用写锁,则 RWMutex 退化为普通的 Mutex.

数据结构
代码语言:txt
AI代码解释
复制
type RWMutex struct {
	w           Mutex        // 互斥锁
	writerSem   uint32       // 关联写锁阻塞队列的信号量
	readerSem   uint32       // 关联读锁阻塞队列的信号量
	readerCount atomic.Int32 // 正常情况下等于介入读锁流程的 goroutine 数量;当 goroutine 接入写锁流程时,该值为实际介入读锁流程的 goroutine 数量减 rwmutexMaxReader
	readerWait  atomic.Int32 // 记录在当前 goroutine 获取写锁前,还需要等待多少个 goroutine 释放读锁
}

  • rwmutexMaxReaders:共享读锁的 goroutine 数量上限,值为 2^29;
  • w: 内置的一把普通互斥锁 sync.Mutex;
  • writerSem:关联写锁阻塞队列的信号量;
  • readerSem:关联读锁阻塞队列的信号量;
  • readerCount:正常情况下等于介入读锁流程的 goroutine 数量;当 goroutine 接入写锁流程时,该值为实际介入读锁流程的 goroutine 数量减 rwmutexMaxReaders.
  • readerWait:记录在当前 goroutine 获取写锁前,还需要等待多少个 goroutine 释放读锁
读锁流程

加锁:

代码语言:txt
AI代码解释
复制
func (rw *RWMutex) RLock() {
    // 将持有或等待写锁的 goroutine +1
    if atomic.AddInt32(&rw.readerCount, 1) < 0 {
        // 走到这里说明有写锁未释放, 所以将 goroutine 放入读锁阻塞队列挂起等待
        runtime_SemacquireMutex(&rw.readerSem, false, 0)
    }
}

这里需要注意的是, 当 readerCount +1 后的值仍然小于0,说明有 goroutine 未释放写锁,因此将当前 goroutine 添加到读锁的阻塞队列中并阻塞挂起

解锁流程:

代码语言:txt
AI代码解释
复制
func (rw *RWMutex) RUnlock() {
    // 先将读锁等待数量 -1
    if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
        // 如果 -1 后的值仍然小于 0, 说明有写锁未释放
        rw.rUnlockSlow(r)
    }
}

func (rw *RWMutex) rUnlockSlow(r int32) {
    // 对 readerCount 的值进行校验
    // 如果未抢占过读锁(r+1 == 0)
    // 或者介入读锁流程的 goroutine 数量达到上限
	if r+1 == 0 || r+1 == -rwmutexMaxReaders {
		fatal("sync: RUnlock of unlocked RWMutex")
	}
	
	// 对 readerWait 减一, 如果为0, 就代表当前 goroutine 是最后一个持有读锁的协程
	// 所以唤醒一个等待写锁的 goroutine
	if rw.readerWait.Add(-1) == 0 {
		// The last reader unblocks the writer.
		runtime_Semrelease(&rw.writerSem, false, 1)
	}
}

写锁流程

加锁流程:

代码语言:txt
AI代码解释
复制
func (rw *RWMutex) Lock() {
    // 用内置互斥锁加锁
    rw.w.Lock()
    // 先对 readerCount 进行减少 -rwmutexMaxReaders 的原子操作
    // 然后加上 rwmutexMaxReaders 给 r 加回去
    r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
    // 如果存在未释放读锁的 goroutine, 给 readerWait 加上读锁的数量, 并将当前 goroutine 挂起
    if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {
        runtime_SemacquireMutex(&rw.writerSem, false, 0)
    }
}

解锁流程:

代码语言:txt
AI代码解释
复制
func (rw *RWMutex) Unlock() {
    // 先给 readerCount 加上 rwmutexMaxReaders
    r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
    if r >= rwmutexMaxReaders {
        fatal("sync: Unlock of unlocked RWMutex")
    }
    // 唤醒读锁阻塞队列的所有 goroutine
    for i := 0; i < int(r); i++ {
        runtime_Semrelease(&rw.readerSem, false, 0)
    }
    rw.w.Unlock()
}

之前说如果有写锁介入,等待读锁的 readerCount 应该是实际介入读锁流程的 goroutine 数量减 rwmutexMaxReader, 在这里也体现了

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Go 并发编程之 Mutex
友情提示:此篇文章大约需要阅读 18分钟0秒,不足之处请多指教,感谢你的阅读。 订阅本站
Meng小羽
2020/11/23
6260
Go 并发编程之 Mutex
【Go】sync.Mutex 源码分析
互斥锁的锁状态由 state 这个 32 的结构表示,这 32 位会被分成两部分:
JuneBao
2022/10/26
2950
听说Mutex源码是出名的不好看,我不信,来试一下
Mutex需要两个变量:key表示锁的使用情况,value 为0表示锁未被持有,1表示锁被持有 且 没有等待者,n表示锁被持有,有n-1个等待者;sema表示等待队列的信号量,sema是个先进先出的队列,用来阻塞、唤醒协程。
薯条的编程修养
2022/08/10
4000
听说Mutex源码是出名的不好看,我不信,来试一下
Mutex的实现
CAS 指令将给定的值和一个内存地址中的值进行比较,如果相等,则用新值替换内存地址中的值。
用户7381369
2020/10/29
1.4K0
面试官:哥们Go语言互斥锁了解到什么程度了?
sync 包下的mutex就是互斥锁,其提供了三个公开方法:调用Lock()获得锁,调用Unlock()释放锁,在Go1.18新提供了TryLock()方法可以非阻塞式的取锁操作:
Golang梦工厂
2022/07/11
4900
面试官:哥们Go语言互斥锁了解到什么程度了?
万字图解| 深入揭秘Golang锁结构:Mutex(下)
    书接上回,在万字图解| 深入揭秘Golang锁结构:Mutex(上)一文中,我们已经研究了Golang mutex V1和V2版本的实现。接下来我们继续研究V3和V4版本的实现。
公众号 云舒编程
2024/01/25
4052
万字图解| 深入揭秘Golang锁结构:Mutex(下)
Go源码解析之sync.Mutex锁
在解释Lock()和Unlock()源码之前我们必须先整体了解下Mutex的设计,不然下面的源码很难看懂。
Orlion
2024/09/02
1090
Go源码解析之sync.Mutex锁
Go中锁的那些姿势,估计你不知道
用俗语来说,锁意味着一种保护,对资源的一种保护,在程序员眼中,这个资源可以是一个变量,一个代码片段,一条记录,一张数据库表等等。
阿伟
2020/02/12
5240
Go中锁的那些姿势,估计你不知道
golang 系列: mutex 讲解
Go 号称是为了高并发而生的,在高并发场景下,势必会涉及到对公共资源的竞争。当对应场景发生时,我们经常会使用 mutex 的 Lock() 和 Unlock() 方法来占有或释放资源。虽然调用简单,但 mutex 的内部却涉及挺多的。今天,就让我们好好研究一下。
lincoln
2021/08/08
9060
Golang 读写锁RWMutex 互斥锁Mutex 源码详解
Golang中有两种类型的锁,Mutex (互斥锁)和RWMutex(读写锁)对于这两种锁的使用这里就不多说了,本文主要侧重于从源码的角度分析这两种锁的具体实现。
LinkinStar
2022/09/01
5910
Golang 读写锁RWMutex 互斥锁Mutex 源码详解
多图详解Go的互斥锁Mutex
在Go的1.9版本中,为了解决等待中的 goroutine 可能会一直获取不到锁,增加了饥饿模式,让锁变得更公平,不公平的等待时间限制在 1 毫秒。
luozhiyun
2020/12/22
5110
多图详解Go的互斥锁Mutex
golang之sync.Mutex互斥锁源码分析
针对Golang 1.9的sync.Mutex进行分析,与Golang 1.10基本一样除了将panic改为了throw之外其他的都一样。
李海彬
2018/12/18
7590
Go 并发实战 -- sync Mutex
在并发编程中我们可以使用channel来协同各个goroutine,但是很多场景我们也是需要使用sync的比如说并发场景下计数器的使用等。 go也为我们提供了一系列的API来协同我们的go协程:
邹志全
2019/07/31
1K0
golang源码分析(6):sync.Mutex sync.RWMutex
默认直接使用 sync.Mutex 或是嵌入到结构体中,state 零值代表未上锁,sema 零值也是有意义的,参考下面源码加锁与解锁逻辑,稍想下就会明白的。另外参考大胡子 dave 的关于零值的文章
golangLeetcode
2022/08/02
1.3K0
信号量,锁和 golang 相关源码分析
本文为社区粉丝原创投稿,再次感谢作者南瓜waniu的分享,欢迎大家在评论区留言和作者讨论,同时也欢迎大家踊跃投稿,分享您的golang语言学习经验!投稿邮箱地址为tougao@golang.ltd
李海彬
2018/10/24
1.7K0
信号量,锁和 golang 相关源码分析
sync.mutex 源代码分析
sync.Mutex是Go标准库中常用的一个排外锁。当一个 goroutine 获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放。
李海彬
2018/12/28
6350
sync.mutex 源代码分析
面试官:哥们Go语言的读写锁了解多少?
互斥锁我们都知道会锁定代码临界区,当有一个goroutine获取了互斥锁后,任何goroutine都不可以获取互斥锁,只能等待这个goroutine将互斥锁释放,无论读写操作都会加上一把大锁,在读多写少场景效率会很低,所以大佬们就设计出了读写锁,读写锁顾名思义是一把锁分为两部分:读锁和写锁,读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的,总结来说:读读不互斥,读写互斥,写写互斥;
Golang梦工厂
2022/07/11
6620
面试官:哥们Go语言的读写锁了解多少?
互斥锁Mutex实现
Mutex即我们常说的互斥锁,也称为排他锁。使用互斥锁,可以限定临界区只能同时有一个goroutine持有。当临界区已经有一个goroutine持有的时候,其他goroutine如果想进入此临界区,会等待或者返回失败。直到持有的goroutine退出临界区,等待goroutine中的某一个才有机会进入临界区执行代码。
数据小冰
2022/08/15
1.5K0
互斥锁Mutex实现
你真的了解 sync.Mutex吗
Mutex是一个互斥的排他锁,零值Mutex为未上锁状态,Mutex一旦被使用 禁止被拷贝。使用起来也比较简单
用户3904122
2022/06/29
3900
你真的了解 sync.Mutex吗
图解golang里面的读写锁实现与核心原理分析了解编程语言背后设计
读写锁区别与互斥锁的主要区别就是读锁之间是共享的,多个goroutine可以同时加读锁,但是写锁与写锁、写锁与读锁之间则是互斥的
8小时
2019/12/21
1.1K0
图解golang里面的读写锁实现与核心原理分析了解编程语言背后设计
相关推荐
Go 并发编程之 Mutex
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档