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 变量进行自增操作,从而避免了数据竞争问题。

相关推荐
梦想很大很大12 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰17 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘21 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤21 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想