Go Mutex

Go Mutex

Mutex互斥锁 )是 Go 语言sync包中最核心的同步原语,用于解决并发资源竞争问题,保证同一时间只有一个 goroutine 能访问临界区资源。

一、介绍

Mutex 全称 Mutual Exclusion(互斥),是一种同步锁

  • 加锁(Lock()):goroutine 获得锁后,其他 goroutine 调用Lock()会阻塞,直到锁被释放。
  • 解锁(Unlock()):只有持有锁的 goroutine 能解锁,否则会引发 panic。
  • 核心作用:保护临界区(多个 goroutine 共享的资源 / 代码段),避免数据竞争导致的不可预期结果。

二、 底层原理

Go 1.20+ 版本

2.1 结构体

2.1.1 对外暴露的Mutex
go 复制代码
type Mutex struct {
	_ noCopy // 禁止值拷贝(编译期检查)

	mu isync.Mutex // 实际的锁实现(内部包)
}
2.1.2 内部真正的 Mutex 实现
go 复制代码
type Mutex struct {
    state int32 // 锁的核心状态(包含locked/woken/starving等)
    sema  uint32// 信号量,用于goroutine阻塞/唤醒
}
2.13 状态常量(位掩码)
go 复制代码
const (
    // 1 << iota 表示按位左移,依次生成 1,2,4...
    mutexLocked = 1 << iota // 第0位:锁是否被持有(1=锁定,0=未锁定)
    mutexWoken              // 第1位:是否有goroutine被唤醒(1=已唤醒,避免重复唤醒)
    mutexStarving           // 第2位:是否处于饥饿模式(1=饥饿模式,0=正常模式)
    mutexWaiterShift = iota // 从第3位开始,存储等待锁的goroutine数量(等待者计数)
    
    starvationThresholdNs = 1e6 // 饥饿模式阈值:1毫秒(1000000纳秒)
)
复制代码
state 字段的二进制结构
二进制: 1    0    1    00010
        ↑    ↑    ↑    ↑
        饥饿 唤醒 锁定 等待者数量(十进制2)
  • mutexLocked(第 0 位):标记锁是否被持有,是最基础的状态。
  • mutexWoken(第 1 位):标记有 goroutine 正在自旋 / 唤醒,避免其他 goroutine 重复唤醒,减少竞争。
  • mutexStarving(第 2 位):标记锁是否进入饥饿模式,解决优先级反转问题。
  • mutexWaiterShift(值为 3):等待锁的 goroutine 数量, 从第 3 位开始存储,计算方式:等待数 = state >> mutexWaiterShift

2.2 正常 / 饥饿模式

模式 核心规则
正常模式 等待者按 FIFO 排队,但唤醒后的等待者需要和新到来的 goroutine 竞争锁(新 goroutine 有 CPU 优势)
饥饿模式 锁直接交给等待队列头部的 goroutine,新 goroutine 不参与竞争,直接排队(保证公平性)
模式切换 等待者等待时间超过 1ms → 进入饥饿模式;等待者是最后一个 / 等待时间 < 1ms → 切回正常模式
2.2.1 Lock () 方法
go 复制代码
func (m *Mutex) Lock() {
	// Fast path: grab unlocked mutex.
	// Fast path: 快速路径,CAS尝试直接获取未锁定的锁
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		// 竞态检测(仅开启-race时生效)
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		// 成功获取锁,直接返回
		return
	}
	// Slow path (outlined so that the fast path can be inlined)
	// Slow path: 慢速路径(锁已被持有/竞争),进入复杂逻辑
	m.lockSlow()
}

逻辑: 优先走快速路径(CAS 直接抢锁),失败则进入慢速路径(自旋→阻塞→唤醒→模式切换),兼顾性能与公平性

2.2.2 lockSlow() 方法
go 复制代码
func (m *Mutex) lockSlow() {
	var waitStartTime int64 // 记录当前goroutine开始等待的时间(用于判断是否进入饥饿模式)
	starving := false // 标记当前goroutine是否处于饥饿状态(等待超过1ms)
	awoke := false  // 标记当前goroutine是否被唤醒(避免重复自旋/阻塞)
	iter := 0 // 自旋次数(控制自旋的最大次数,避免无限自旋)
	old := m.state // 缓存当前锁的state状态(减少原子读取次数)
	for {
		// Don't spin in starvation mode, ownership is handed off to waiters
		// so we won't be able to acquire the mutex anyway.
		// if old&(mutexLocked|mutexStarving) == mutexLocked   锁被持有且非饥饿模式(饥饿模式不自旋)
		//  runtime_canSpin(iter) → 满足自旋条件(CPU核数>1、有空闲P、自旋次数<4等)
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
			// Active spinning makes sense.
			// Try to set mutexWoken flag to inform Unlock
			// to not wake other blocked goroutines.
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				// 标记当前goroutine已唤醒,避免重复设置
				awoke = true
			}
			// 执行自旋(空转,消耗CPU但不阻塞,抢锁窗口期)
			runtime_doSpin()
			// 自旋次数+1
			iter++
			// 重新读取state(自旋期间可能被其他goroutine修改)
			old = m.state
			continue // 继续循环,尝试抢锁
		}
		new := old // 基于旧状态初始化新状态
		// Don't try to acquire starving mutex, new arriving goroutines must queue.
		// 非饥饿模式 → 尝试抢占锁(设置mutexLocked位)
		if old&mutexStarving == 0 {
			new |= mutexLocked // 标记锁为锁定状态(尝试抢锁)
		}
		// 锁被持有 或 处于饥饿模式 → 等待者数量+1
		if old&(mutexLocked|mutexStarving) != 0 {
			// 等待者数量左移3位后+1(等价于等待数+1)
			new += 1 << mutexWaiterShift
		}
		// The current goroutine switches mutex to starvation mode.
		// But if the mutex is currently unlocked, don't do the switch.
		// Unlock expects that starving mutex has waiters, which will not
		// be true in this case.
		// 当前goroutine饥饿(等待>1ms)且锁被持有 → 切换为饥饿模式
		if starving && old&mutexLocked != 0 {
			new |= mutexStarving // 设置饥饿模式位
		}
		// 当前goroutine被唤醒 → 清除mutexWoken位(重置唤醒标记)
		if awoke {
			// The goroutine has been woken from sleep,
			// so we need to reset the flag in either case.
			// // 校验:如果新状态没有唤醒位,说明状态不一致,直接panic
			if new&mutexWoken == 0 {
				throw("sync: inconsistent mutex state")
			}
			// 清除唤醒位(等价于 new = new & ^mutexWoken)
			new &^= mutexWoken
		}
		// CAS原子操作:尝试将state从old修改为new(抢锁核心)
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
		//成功修改state后,判断是否已获取锁
		// old&(mutexLocked|mutexStarving) == 0 → 锁未被持有且非饥饿,说明CAS直接抢到锁
			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=true → 把当前goroutine放到等待队列头部(LIFO),用于饥饿模式优先处理
			queueLifo := waitStartTime != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()// 记录开始等待的时间(纳秒)
			}
			// 阻塞当前goroutine,等待信号量(被Unlock()唤醒)
			runtime_SemacquireMutex(&m.sema, queueLifo, 2)
			// 唤醒后:判断是否饥饿(等待时间>1ms)
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			// 重新读取唤醒后的state
			old = m.state
			// 被唤醒后发现锁处于饥饿模式 → 直接获取锁并处理状态
			if old&mutexStarving != 0 {
				// If this goroutine was woken and mutex is in starvation mode,
				// ownership was handed off to us but mutex is in somewhat
				// inconsistent state: mutexLocked is not set and we are still
				// accounted as waiter. Fix that.
				// 校验:饥饿模式下的状态必须合法(锁未被持有、有等待者、无唤醒位)
				if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
					throw("sync: inconsistent mutex state")
				}
				// delta = mutexLocked(设置锁定位) - 1<<mutexWaiterShift(等待数-1)
				delta := int32(mutexLocked - 1<<mutexWaiterShift)
				// 退出饥饿模式的条件:当前goroutine不饥饿 或 是最后一个等待者
				if !starving || old>>mutexWaiterShift == 1 {
					// Exit starvation mode.
					// Critical to do it here and consider wait time.
					// Starvation mode is so inefficient, that two goroutines
					// can go lock-step infinitely once they switch mutex
					// to starvation mode.
					delta -= mutexStarving // 清除饥饿模式位
				}
				atomic.AddInt32(&m.state, delta) // 原子修改state,完成锁获取
				break // 抢锁成功,退出循环
			}
			// 被唤醒后处于正常模式 → 标记为唤醒状态,重置自旋次数,继续循环抢锁
			awoke = true
			iter = 0
		} else {
		// CAS失败(state被其他goroutine修改)→ 重新读取state,继续循环
			old = m.state
		}
	}
 // 竞态检测
	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}
复制代码
graph TD
    A["开始 lockSlow()"] --> B[初始化变量]
    B --> C[循环开始]
    
    C --> D{可以自旋?}
    D -- 是 --> E[执行自旋操作]
    E --> C
    
    D -- 否 --> F[准备新状态: new = old]
    F --> G{是否为饥饿模式?}
    G -- 是 --> H[增加等待者数量]
    G -- 否 --> I[尝试抢占锁: new |= mutexLocked]
    I --> H
    
    H --> J{锁是否被持有?}
    J -- 是 --> K[增加等待者数量: new += 1<<mutexWaiterShift]
    J -- 否 --> L{是否饥饿?}
    K --> L
    
    L -- 是 --> M[切换为饥饿模式: new |= mutexStarving]
    L -- 否 --> N{是否被唤醒?}
    M --> N
    
    N -- 是 --> O[清除唤醒标志: new &^= mutexWoken]
    N -- 否 --> P[CAS尝试修改状态]
    O --> P
    
    P --> Q{CAS成功?}
    Q -- 否 --> R[重新读取状态: old = m.state]
    R --> C
    
    Q -- 是 --> S{直接获取锁?}
    S -- 是 --> T[成功获取锁,退出循环]
    
    S -- 否 --> U{已在等待?}
    U -- 否 --> V[记录等待开始时间]
    V --> W[阻塞等待]
    U -- 是 --> W
    
    W --> X{是否进入饥饿状态?}
    X --> Y[重新读取状态: old = m.state]
    Y --> Z{是否为饥饿模式?}
    
    Z -- 是 --> AA[校验饥饿模式状态]
    AA --> BB[计算状态变化: delta = mutexLocked - 1<<mutexWaiterShift]
    BB --> CC{退出饥饿模式?}
    CC -- 是 --> DD[清除饥饿模式: delta -= mutexStarving]
    DD --> EE[原子更新状态]
    CC -- 否 --> EE
    EE --> FF[成功获取锁,退出循环]
    
    Z -- 否 --> GG[设置唤醒状态: awoke = true]
    GG --> HH[重置自旋次数: iter = 0]
    HH --> C
    
    T --> II[结束]
    FF --> II

lockSlow() 核心行为

  1. 自旋(Spin)
  • 锁处于正常模式非饥饿),饥饿模式下锁会直接移交等待者,自旋无意义;
  • 满足 runtime_canSpin 条件
    • CPU 核心数 > 1(否则自旋浪费 CPU)
    • 当前 goroutine 所在的 P(处理器)有空闲的 M(线程)
    • 自旋次数 iter < 4(默认最大自旋次数,避免无限自旋)
  1. CAS 修改 state 并处理阻塞 / 唤醒
  • (1)CAS 成功但未直接抢锁 → 进入阻塞
    • queueLifo:
      • waitStartTime != 0 表示当前 goroutine 之前已经等待过,此时会被放到等待队列头部(LIFO),优先被唤醒(解决饥饿问题)
      • 首次等待则放到队列尾部(FIFO)
  • (2)被唤醒后判断是否饥饿
    • runtime_nanotime()-waitStartTime > starvationThresholdNs
      计算等待时间是否超过 1msstarvationThresholdNs=1e6纳秒),如果是则标记 starving=true
  • (3)饥饿模式下的锁获取(公平性保障)
    • 饥饿模式下,锁会直接移交 给当前 goroutine,无需竞争`。避免 "新 goroutine 无限抢锁,老 goroutine 饿死 "
      • delta = mutexLocked - 1<<mutexWaiterShift:设置锁定位(抢锁)+ 等待数 - 1
      • 如果是最后一个等待者 / 当前 goroutine 不饥饿 ,清除饥饿模式位(切回正常模式
      • 原子修改 state 后,直接 break 抢锁成功
  • (4)正常模式下的唤醒处理
    • 标记 awoke=true,重置自旋次数 iter=0,回到循环开头重新自旋抢锁
  1. 阻塞等待:自旋失败后,通过 sema 信号量将 goroutine 加入等待队列,进入阻塞状态(让出 CPU)
  2. 模式切换 :被唤醒后检查等待时间,超过 1ms 则切换到饥饿模式,保证公平性
2.2.3 Unlock() 方法
go 复制代码
func (m *Mutex) Unlock() {
// 竞态检测逻辑
	if race.Enabled {
		_ = m.state
		race.Release(unsafe.Pointer(m))
	}

	// Fast path: drop lock bit.
	// 快速路径(Fast Path):无等待者的锁释放
	// 用 atomic.AddInt32(原子加法)将 state 减去 mutexLocked(值为 1),本质是清除第 0 位的
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		// Outlined slow path to allow inlining the fast path.
		// To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock.
		m.unlockSlow(new)
	}
}
  • new = 0
    • 说明没有等待者、非饥饿模式、无唤醒的 goroutine,锁已完全释放,无需后续操作,直接返回
  • new != 0
    • 有等待锁的 goroutine(等待者数量 > 0);
    • 锁处于饥饿模式(mutexStarving 位为 1);
    • 有被唤醒的 goroutine(mutexWoken 位为 1)
2.2.4 unlockSlow() 方法
go 复制代码
func (m *Mutex) unlockSlow(new int32) {
 // 非法解锁校验
 //  new 是释放锁定位后的 state 值
 // new + mutexLocked 还原释放前的 state
 // & mutexLocked 提取还原后 state 的第 0 位(锁定位)
 // 若结果为 0,说明释放前锁的锁定位就是 0(未加锁),属于非法解锁
	if (new+mutexLocked)&mutexLocked == 0 {
		fatal("sync: unlock of unlocked mutex")
	}
	// 非饥饿
	if new&mutexStarving == 0 {
	// 缓存当前 state,减少原子读取
		old := new
		for {
			// If there are no waiters or a goroutine has already
			// been woken or grabbed the lock, no need to wake anyone.
			// In starvation mode ownership is directly handed off from unlocking
			// goroutine to the next waiter. We are not part of this chain,
			// since we did not observe mutexStarving when we unlocked the mutex above.
			// So get off the way.
			// old>>mutexWaiterShift == 0 → 无等待者
			// old&(mutexLocked|mutexWoken|mutexStarving) != 0 → 锁被持有/有唤醒的 goroutine/进入饥饿模式
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// 计算新 state:等待者数量-1 + 标记 mutexWoken(避免重复唤醒)
			// Grab the right to wake someone.
			new = (old - 1<<mutexWaiterShift) | mutexWoken
			// CAS 原子修改 state:保证多 goroutine 下状态一致
			if atomic.CompareAndSwapInt32(&m.state, old, new) {
			// // 唤醒一个等待的 goroutine(非 FIFO,正常模式下竞争抢锁)
				runtime_Semrelease(&m.sema, false, 2)
				return
			}
			// CAS 失败(state 被其他 goroutine 修改)→ 重新读取 state 重试
			old = m.state
		}
	} else {
		// Starving mode: handoff mutex ownership to the next waiter, and yield
		// our time slice so that the next waiter can start to run immediately.
		// Note: mutexLocked is not set, the waiter will set it after wakeup.
		// But mutex is still considered locked if mutexStarving is set,
		// so new coming goroutines won't acquire it.
		// 饥饿模式的核心设计是公平优先,锁直接移交等待最久的 goroutine,而非让新 goroutine 竞争
		// true 表示按 FIFO 唤醒(严格唤醒等待队列头部的 goroutine,即等待最久的那个)
		// 被唤醒的 goroutine 无需竞争,直接获取锁,解决 "老 goroutine 饿死" 的问题
		runtime_Semrelease(&m.sema, true, 2)
	}
}
go 复制代码
graph TD
    A[进入unlockSlow] --> B[非法解锁校验,失败则fatal]
    B --> C{是否饥饿模式?};
    C -->|正常模式| D[初始化old=new,进入循环]
    D --> E{无等待者/锁被持有/有唤醒goroutine?};
    E -->|是| F[返回,无需唤醒]
    E -->|否| G[计算new=等待数-1+标记唤醒位]
    G --> H{CAS修改state成功?};
    H -->|是| I[唤醒一个goroutine(非FIFO),返回]
    H -->|否| J[更新old=最新state,回到E]
    C -->|饥饿模式| K[唤醒队列头部goroutine(FIFO),返回]

unlockSlow() 核心流程

  1. 非法解锁校验
    校验 "解锁的是未加锁的 Mutex"(非法操作),直接终止程序
  2. 正常模式逻辑 :仅在 "有等待者、无唤醒 goroutine " 时唤醒一个 goroutine(非 FIFO),优先保证性能
  3. 饥饿模式逻辑 :严格按FIFO唤醒队列头部的 goroutine,锁直接移交,保证公平性;

三、使用场景

1. 保护共享变量的读写(最基础场景)

多个 goroutine 同时读写同一个变量时,必须用 Mutex 避免数据竞争,比如计数器、全局配置、缓存等。

go 复制代码
package main

import (
    "sync"
    "fmt"
)

var (
    count int
    mu    sync.Mutex
    wg    sync.WaitGroup
)

func increment() {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        mu.Lock()   // 加锁保护count
        count++     // 临界区:共享变量修改
        mu.Unlock() // 解锁
    }
}

func main() {
    wg.Add(2)
    go increment()
    go increment()
    wg.Wait()
    fmt.Println("最终计数:", count) // 正确输出2000(无锁会小于2000)
}

2. 保护复杂数据结构的操作

对于 map、slice 等非线程安全的复合数据结构,若多个 goroutine 同时执行 "读 + 写" 或 "写 + 写" 操作,必须用 Mutex 包裹完整的操作逻辑(而非单个步骤)

go 复制代码
package main

import (
	"fmt"
	"sync"
)

type SafeMap struct {
	m  map[string]int
	mu sync.Mutex
}

// 新增/更新key(写操作)
func (sm *SafeMap) Set(key string, val int) {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	sm.m[key] = val
}

// 获取key(读操作)
func (sm *SafeMap) Get(key string) (int, bool) {
	sm.mu.Lock()
	defer sm.mu.Unlock()
	val, ok := sm.m[key]
	return val, ok
}

func main() {
	sm := &SafeMap{m: make(map[string]int)}
	var wg sync.WaitGroup

	// 10个goroutine并发写
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			sm.Set(fmt.Sprintf("key%d", n), n)
		}(i)
	}

	wg.Wait()
	// 读取验证
	for i := 0; i < 10; i++ {
		val, ok := sm.Get(fmt.Sprintf("key%d", i))
		fmt.Printf("key%d: %d, exists: %t\n", i, val, ok)
	}
}

四、注意事项

1. 必须保证 Lock/Unlock 配对(最核心)

错误场景 :加锁后未解锁(比如 panic 导致 Unlock 未执行),会导致其他 goroutine 永久阻塞(死锁);
正确做法 :加锁后立即用 defer mu.Unlock(),确保函数退出时必解锁

2. 禁止重复加锁 / 解锁

  • 重复加锁 :同一 goroutine 连续 Lock() 会直接死锁
  • 重复解锁:未加锁 / 已解锁时调用 Unlock() 会 panic

3. 缩小锁粒度(性能优化核心)

Mutex 会串行化临界区代码,锁粒度越大,并发性能越差。只锁必要的代码段,而非整个函数

4. 禁止拷贝 Mutex(编译期检查)

Mutex 包含noCopy结构体,go vet 会检查是否值拷贝,拷贝后原锁的状态会丢失,导致同步失效

五、Mutex 和 RWMutex 的区别

特性 Mutex RWMutex
加锁规则 同一时间仅一个 goroutine 加锁 读锁:多 goroutine 并发;写锁:仅一个 goroutine
互斥关系 读写 / 写写 / 读读均互斥 读写 / 写写互斥,读读不互斥
性能 简单无额外开销 读多写少场景性能更高,有状态管理开销
缺点 读并发性能差 写锁阻塞所有读锁,状态管理有开销
适用场景 读写频率相当、写操作占比高 读多写少(如配置读取、缓存查询)
相关推荐
人间打气筒(Ada)3 小时前
如何使用 Go 更好地开发并发程序?
开发语言·后端·golang
yuanlaile3 小时前
Go-Zero高性能Web+微服务全集解析
微服务·golang·go-zero·go微服务
呆萌很6 小时前
【GO】for 循环练习题
golang
F1FJJ6 小时前
开源实践:用 Go 实现浏览器直连内网 RDP/SSH/VNC
运维·网络·网络协议·网络安全·golang·ssh
呆萌很6 小时前
【GO】switch 练习题
golang
添尹19 小时前
Go语言基础之变量和常量
golang
参.商.1 天前
【Day43】49. 字母异位词分组
leetcode·golang
参.商.1 天前
【Day45】647. 回文子串 5. 最长回文子串
leetcode·golang
AMoon丶1 天前
Golang--内存管理
开发语言·后端·算法·缓存·golang·os