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

相关推荐
不会写DN3 小时前
Golang中的map的key可以是哪些类型?可以嵌套map吗?
后端·golang·go
审判长烧鸡4 小时前
GO分层架构【4】Repository获取 *gorm.DB
go·分层架构·结构体注入
我叫黑大帅6 小时前
其实跨域问题是后端来解决的? CORS
后端·面试·go
审判长烧鸡10 小时前
GO分层架构【2】使用GIN与GORM
go·分层架构
Go_error1 天前
Go channel 数据聚合
后端·go
stark张宇1 天前
Go 语言实现安全的分享链接:AES 加密 + SHA256 签名 + 过期防重放
后端·go
我叫黑大帅2 天前
Golang中的map的key可以是哪些类型?可以嵌套map吗?
后端·面试·go
用户095367515832 天前
Go :如何声明变量(var)与常量(const)
后端·go
FelixBitSoul2 天前
Go 语言面试深度全攻略:从工程化到底层原理,一文通杀
后端·go
你有医保你先上2 天前
Elasticsearch Go 客户端
后端·elasticsearch·go