sync.Mutex 原理浅析

一、概述

并发原语是在并发编程中用于控制和协调多个并发执行单元(如线程、进程、协程 )之间操作的基本操作单元或机制,能确保并发程序正确、高效运行,避免数据竞争、死锁等问题 。golang 常见的并发原语如下:

  • 锁(Mutex): sync.Mutex
  • 读写锁(RWMutex): sync.RWMutex
  • 原子操作(Atomic): sync/atomic
  • 信号量(Semaphore): 使用channel实现
  • 条件变量(Condition Variable): sync.Cond配合互斥锁
  • 通道(Channel): channel

今天,我们将针对 golang 中的常用的 sync.Mutex 展开讲解。

二、sync.Mutex原理浅析

2.1 核心数据结构

golang 复制代码
type Mutex struct {
    state int32  // 状态字段 
    sema  uint32 // 信号量
}
  • sema: 是一个信号量,用于实现 goroutine 的阻塞与唤醒操作。
  • state: 是一个 32 位整数,用于表示锁的状态。不同的位有着不同的含义,涵盖锁是否被持有、是否有等待者、是否处于饥饿模式等。
    • 第 0 位(mutexLocked): 锁定位,表示锁是否被持有。
      • 1: 代表锁已被持有状态;
      • 0: 代表锁处于未被持有状态;
    • 第 1 位( mutexWoken): 唤醒位,表示是否有被唤醒的协程(用于避免重复唤醒)。
      • 1: 代表有 goroutine 已被唤醒,正在尝试获取锁;
    • 第 2 位(mutexStarving): 饥饿位,表示当前是否处于饥饿模式。
      • 1: 代表锁处于饥饿模式;
      • 0: 代表锁处于正常模式;
    • 剩余 29 位: 等待者计数,表示记录等待锁的协程数量(通过信号量排队)。

2.2 操作模式

互斥锁可以处于两种操作模式:正常模式和饥饿模式。正常模式 具有相当好的性能,因为即使存在处于阻塞状态的等待协程,一个协程也可以连续多次获取到互斥锁。而饥饿模式对于防止出现尾部延迟的极端情况非常重要。

2.2.1 正常模式

  • 在正常模式下,等待获取锁的 goroutine 会按照先进先出(FIFO)的顺序排队。
  • 被唤醒的等待者并不直接拥有锁,而是要和新到来的 goroutine 竞争锁的所有权。新到来的 goroutine 因已经在 CPU 上运行,所以具有一定优势,被唤醒的等待者可能竞争失败,此时它会被重新排到等待队列的队首。
  • 若一个等待者在超过 1 毫秒的时间内都未能获取到锁,锁就会切换到饥饿模式。

2.2.2 饥饿模式

  • 在饥饿模式下,锁的所有权会直接从解锁的 goroutine 传递给等待队列队首的 goroutine。
  • 新到来的 goroutine 即便发现锁看似处于解锁状态,也不会尝试获取锁,而是直接排到等待队列的队尾。
  • 当一个等待者获得锁后,若它是等待队列中的最后一个等待者,或者其等待时间少于 1 毫秒,锁就会切换回正常模式。

2.2.3 模式切换

如果一个等待协程获得了互斥锁的所有权,并且发现以下两种情况之一:

  1. 它是等待队列中的最后一个协程;
  2. 它的等待时间少于 1 毫秒,那么互斥锁会切换回正常操作模式。

2.3 操作流程

2.3.1 加锁(Lock())

  • 快速路径: 若锁未被持有(锁定位=0),直接通过CAS获取;
  • 慢路径:
    1. 若可自旋(多核、正常模式、尝试次数未超限),则自旋等待锁释放。
    2. 自旋失败后,进入等待队列并递增等待计数。
    3. 若等待时间过长,则触发饥饿模式。

2.3.2 解锁(Unlock())

  • 快速路径: :清除 mutexLocked 位,若此时无等待协程,直接返回。
  • 慢路径: :根据当前模式唤醒协程。
    • 正常模式: 唤醒队列头部协程,与新协程公平竞争。
    • 饥饿模式: 直接将锁交给队列头部协程,新协程不参加自选直接进入等待队列。

三、代码示例

golang 复制代码
package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mutex   sync.Mutex
)

func increment() {
    mutex.Lock()
    defer mutex.Unlock()
    counter++
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
       wg.Add(1)
       go func() {
          defer wg.Done()
          increment()
       }()
    }
    wg.Wait()
    // 期望输出:Counter: 1000
    fmt.Println("Counter:", counter)
}

示例解释说明: 在上述代码中,increment 函数借助 mutex.Lock()mutex.Unlock() 来确保同一时刻仅有一个 goroutine 能够对 counter 变量进行自增操作,从而避免了数据竞争问题。

相关推荐
我不会起名字32216 小时前
一天一道力扣Hot100(36):深度优先算法---组合总和
数据结构·c++·后端·python·算法·go
名字还没想好☜1 天前
Go context.AfterFunc 实战(Go 1.21):context 一取消就自动跑清理,告别手写 goroutine 监听 Done
后端·golang·go
liyinchi19883 天前
微信小程序支付遇到“由于小程序违规,支付功能暂时无法使用” 解决办法
java·微信小程序·go
天天喝旺仔3 天前
Go 泛型实战:从类型参数、约束到可复用泛型容器与函数
数据结构·算法·容器·go
gospace3 天前
【无标题】
go·gb28181·gowvp
我不会起名字3224 天前
一天一道算法题(35):电话号码的字母组合
java·数据结构·后端·python·leetcode·go·回溯
江湖十年4 天前
Go GOPROXY 兜底链的坑:从一次诡异的 CI lint 失败说起
后端·面试·go
秋风点枝4 天前
gogogo!go语言感悟大观--打卡day01
go·devops
探索云原生4 天前
KubeClipper 1.7.0 发布:Operation 优化与 Kubernetes 1.37 支持
linux·docker·云原生·kubernetes·go
右耳朵猫AI4 天前
Go周刊2026W37 | Ebitengine 纯 Go 化、simd 重写 TurboPFor、json/v2 落地
后端·微服务·go