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

相关推荐
Dobby_0518 小时前
【Go】C++ 转 Go 第(二)天:变量、常量、函数与init函数
vscode·golang·go
光头闪亮亮19 小时前
Golang使用gofpdf库和barcode库创建PDF原材料二维码标签【GBK中文或UTF8】及预览和打印
go
光头闪亮亮21 小时前
go-fitz库-PDF文件所有页转换到HTML及从HTML中提取图片的示例教程
go
用户855651414461 天前
环信http请求失败排查
go
_码力全开_2 天前
P1005 [NOIP 2007 提高组] 矩阵取数游戏
java·c语言·c++·python·算法·矩阵·go
程序员爱钓鱼2 天前
Python编程实战 · 基础入门篇 | Python程序的运行方式
后端·go
光头闪亮亮3 天前
gozxing库-对图片中多个二维码进行识别的完整示例教程
go
召摇3 天前
在浏览器中无缝运行Go工具:WebAssembly实战指南
后端·面试·go
王中阳Go4 天前
我发现不管是Java还是Golang,懂AI之后,是真吃香!
后端·go·ai编程
半枫荷4 天前
二、Go语法基础(基本语法)
go