go并发与锁之sync.Mutex入门

sync.Mutex

原理:一个共享的变量,哪个线程握到了,哪个线程可以执行代码

功能:一个性能不错的悲观锁,使用方式和Java的ReentrantLock很像,就是手动Lock,手动UnLock。

使用例子:

go 复制代码
var mu sync.Mutex // 管理协程用的,主要是让协程同意结束后再运行调用协程
var cnt int
func add() {cnt++}
var wg sync.WaitGroup
func main() {
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			defer mu.Unlock()
			mu.Lock()
			add()
		}()
	}
	wg.Wait()
	fmt.Print(cnt)
}
实现原理:

直接看源码好了,

go 复制代码
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()
}

lockSlow()是一个逻辑很多的方法,具体逻辑是:

  1. 自旋尝试:
    • 若当前是正常模式且锁持有时间较短,当前goroutine会自旋(循环检查锁状态),尝试避免立即阻塞。
    • 自旋条件:多核CPU、当前未处于饥饿模式、等待队列为空或自旋次数未超过阈值。
  2. 更新等待计数:
    • 通过原子操作增加state中的等待goroutine计数(高30位)。
  3. 进入阻塞或饥饿模式:
    • 正常模式 :若自旋失败,将当前goroutine加入信号量等待队列(sema),并调用runtime_SemacquireMutex阻塞。
    • 饥饿模式:若当前goroutine等待时间超过阈值(1ms),触发饥饿模式。此时新来的goroutine直接进入队列尾部,不再自旋。
go 复制代码
func (m *Mutex) lockSlow() {
    // 初始化变量操作,省略...
	for {
        // 这部分处理自旋尝试获取锁的逻辑
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			 // 省略...
			runtime_doSpin()
			continue
		}
		new := old
        // 如果不是饥饿模式,尝试获取锁(new |= mutexLocked)
		if old&mutexStarving == 0 {
			new |= mutexLocked
		}
        // 如果锁已被占用或处于饥饿模式,增加等待者计数(new += 1 << mutexWaiterShift)
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		// 如果当前 goroutine 处于饥饿状态且锁被占用,切换到饥饿模式(new |= mutexStarving)
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving
		}
        // 如果当前 goroutine 是被唤醒的:确保 mutexWoken 标志已设置(否则抛出异常);清除 mutexWoken 标志(new &^= mutexWoken)
		if awoke {
			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 
			}
            // 决定排队位置:如果是第一次等待(waitStartTime == 0),记录开始等待时间;否则使用 LIFO 顺序(queueLifo = true)
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
            // runtime_SemacquireMutex 将 goroutine 放入等待队列并阻塞
			runtime_SemacquireMutex(&m.sema, queueLifo, 2)
            // 被唤醒后:检查是否等待超时(超过 1ms),更新饥饿状态;重新读取锁状态
			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")
				}
                // 计算状态增量: 设置 mutexLocked;减少等待者计数;如果不再饥饿或只有一个等待者,退出饥饿模式
				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
		}
	}
}

Goroutine A 获取锁(Lock()快速路径成功)。

Goroutine B 尝试获取锁,进入慢速路径:

自旋数次后失败,增加等待计数,进入队列阻塞。

Goroutine A 释放锁(Unlock()):

唤醒Goroutine B,新来的Goroutine C可与B竞争锁。

Goroutine B 等待超过1ms,触发饥饿模式。

Goroutine C 新到达,直接进入队列尾部,不自旋。

Goroutine A 释放锁:

直接将锁交给队列头部的Goroutine B。

Goroutine B 释放锁后,若队列中无等待者,退出饥饿模式。

作者:ShanekAI

链接:https://juejin.cn/post/7488246529430487077

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

整体逻辑大致来说就是:Go 的 sync.Mutex 在竞争不激烈时,会采用短暂的 自旋锁 机制。自旋锁允许 Goroutine 在一小段时间内忙等待,而不是立即进入阻塞状态。这种策略避免了频繁的上下文切换开销。如果激烈的话,就进入饥饿模式,更改了逻辑,在饥饿模式里,停止自旋,直接将当前协程加入等待队列。当前线程执行完毕了,如果是饥饿模式,会把队列里第一个拿出来唤醒。

名词解释:

自旋:就是忙等,就是最简单的例子:

go 复制代码
for state == 1{} // 不停地遍历,就好像在不停地自我旋转一样;直到state被其他线程修改了,才停止
相关推荐
菥菥爱嘻嘻1 小时前
JS手写代码篇---Pomise.race
开发语言·前端·javascript
南瓜胖胖1 小时前
【R语言编程绘图-调色】
开发语言·r语言
lanbing2 小时前
非常适合初学者的Golang教程
开发语言·后端·golang
stormsha3 小时前
GO语言进阶:掌握进程OS操作与高效编码数据转换
开发语言·数据库·后端·golang·go语言·源代码管理
B1nna3 小时前
SpringBoot项目快速打包与部署,War包⽅式打包部署与Jar包⽅式打包部署两种方式
spring boot·后端·jar
老神在在0014 小时前
javaEE1
java·开发语言·学习·java-ee
魔道不误砍柴功4 小时前
《接口和抽象类到底怎么选?设计原则与经典误区解析》
java·开发语言
我是李武涯5 小时前
C++ 条件变量虚假唤醒问题的解决
开发语言·c++·算法
编码小笨猪6 小时前
[ Qt ] | 常用控件(三):
开发语言·qt
Bioinfo Guy6 小时前
R包安装报错解决案例系列|R包使用及ARM架构解决data.table安装错误问题
开发语言·arm开发·r语言