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

相关推荐
一丝晨光1 天前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
陌尘(MoCheeen)2 天前
技术书籍推荐(002)
java·javascript·c++·python·go
白泽来了4 天前
字节大模型应用开发框架 Eino 全解(一)|结合 RAG 知识库案例分析框架生态
开源·go·大模型应用开发
致于数据科学家的小陈5 天前
Go 层级菜单树转 json 处理
python·go·json·菜单树·菜单权限·children
白总Server6 天前
Golang领域Beego框架的中间件开发实战
服务器·网络·websocket·网络协议·udp·go·ssl
ん贤7 天前
GoWeb开发
开发语言·后端·tcp/ip·http·https·go·goweb
纪元A梦7 天前
华为OD机试真题——荒岛求生(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
java·c语言·javascript·c++·python·华为od·go
chxii9 天前
3.2goweb框架GORM
go
42fourtytoo10 天前
从0开始建立Github个人博客(hugo&PaperMod)
运维·服务器·python·go·github
xuhe211 天前
[tldr] GO语言异常处理
go·error