golang中的并发同步机制

atomic channel sync

Go 语言标准库中,原生提供的可直接使用的锁均在sync包中

atomic

原子操作是计算机硬件来提供支持的。比如x86的LOCK前缀指令

作用就是原子化的修改某个值。

sync

核心是两类基础锁,sync.Mutex(互斥锁)和 sync.RWMutex(读写锁)。

sync.Locker 是 Go 语言标准库定义的同步锁接口,互斥锁和读写锁都实现了sync.Locker

sync.Mutex(互斥锁)

Mutex是会操作goroutine的,这是它不同于atomic(只修改某个值)的地方。

严格互斥,一个时间点上只能由一个goroutine持有

底层结构

// 它的底层结构是这样的:
type Mutex Struct {
    state int32 // 锁的复合状态
    sema uint32 // 信号量,用于goroutine的阻塞与唤醒
}

state: 按照bit位拆分的复合状态,低3位是状态标记,高29位是记录等待锁的goroutine数量。

  • 第 0 位mutexLocked:标记锁是否被持有,1 为已锁定;
  • 第 1 位mutexWoken:标记是否有 goroutine 被唤醒正在抢锁;
  • 第 2 位mutexStarving:标记锁是否处于饥饿模式;
  • 高 29 位:等待锁的 goroutine 总数。

sema:底层基于操作系统 futex 机制(两个系统调用)实现的信号量,负责阻塞抢锁失败的 goroutine,锁释放时唤醒等待队列。

核心运行模式

  • 正常模式(默认)新 goroutine 抢锁时,先通过 CAS 原子操作尝试直接获取锁,失败后会在满足条件(多核 CPU、自旋次数 < 4 次、当前 goroutine 未被抢占)时进行自旋抢锁,减少 goroutine 上下文切换开销;自旋失败后,goroutine 进入信号量等待队列,按 FIFO 顺序排队阻塞;锁释放时,唤醒队列头部的 goroutine,但唤醒的 goroutine 不会直接持有锁,会和新到来的自旋 goroutine 再次竞争锁。优势:最大化利用 CPU,降低调度开销;劣势:极端场景下会出现 goroutine 长时间抢不到锁的饥饿问题。
  • 饥饿模式当某个 goroutine 等待锁的时间超过 1ms,锁会自动切换为饥饿模式;此模式下,锁的所有权会直接从解锁的 goroutine 交给等待队列头部的 goroutine,新到来的 goroutine 不会自旋、也不会尝试抢锁,直接进入等待队列尾部;当等待的 goroutine 等待时间小于 1ms,或它是队列中最后一个等待者,锁退出饥饿模式,切回正常模式。

⚠️⚠️⚠️ Mutex不可重入(同一 goroutine 重复加锁会导致死锁)、不可复制已使用的锁、必须成对加解锁,解锁未加锁的锁会直接 panic。

sync.RWMutex(读写互斥锁)

RWMutex是机遇Mutex实现的,只是针对读多写少的场景进行了优化。

底层结构

type RWMutex struct {
    w Mutex // 内置互斥锁,保证写操作之间的互斥
    writerSem uint32 // 写者信号量,阻塞等待读锁释放的写goroutine 
    readerSem uint32 // 读者信号量,阻塞等待写锁释放的读goroutine 
    readerCount int32 // 读锁计数器,负数表示有写者等待/持有锁 
    readerWait int32 // 写者需等待的前置读锁数量 
 }

  • readerCount:正数为当前持有读锁的 goroutine 数量,减去常量rwmutexMaxReaders(1<<30)后变为负数,标记有写者正在等待;
  • readerWait:写者到来前已持有读锁的 goroutine 数量,写者必须等待这些读者全部释放才能持有写锁,避免写者饥饿。
  • Go 内核里有一个全局哈希表: key = 信号量地址(&readerSem)value = 挂在此地址的 goroutine 链表

核心运行机制

通过readerCount的正负值标记写者状态,同时通过计数器保障写优先级,避免持续读操作导致写锁永远无法获取。

  • 加写锁Lock()流程调用内置 Mutex 的Lock(),保证多个写操作之间完全互斥;原子操作将readerCount减去rwmutexMaxReaders,置为负数,标记有写者等待,阻止后续新的读锁加锁;将当前剩余的读锁数量赋值给readerWait,若readerWait>0,当前 goroutine 阻塞在writerSem信号量上,等待所有前置读者释放读锁;所有前置读者释放后,成功持有写锁,独占临界区。
  • 释放写锁Unlock()流程原子操作将readerCount加上rwmutexMaxReaders,恢复为正数,标记写锁即将释放;唤醒所有阻塞在readerSem上的读 goroutine;释放内置 Mutex 的Unlock(),允许下一个写者竞争。
  • 加读锁RLock()流程原子操作将readerCount+1,若结果 >0,说明无写者等待 / 持有,直接加读锁成功;若结果 <= 0,说明有写者正在等待 / 持有,当前 goroutine 阻塞在readerSem信号量上,等待写锁释放后被唤醒。
  • 释放读锁RUnlock()流程原子操作将readerCount-1,若结果 >0,释放成功直接返回;若结果 <= 0,说明有写者正在等待,将readerWait-1;当readerWait变为 0 时,唤醒阻塞在writerSem上的写者。

⚠️⚠️⚠️ :不支持读锁升级为写锁(会导致死锁)、支持写锁降级为读锁、不可重入、不可复制、必须成对加解锁。

Channel

实现原理(CSP 模型的核心实现)

  • channel 是一种类型安全的队列,用于在 goroutine 之间传递数据发送信号,天然支持并发安全,无需加锁。

1. 核心机制

  • 底层实现:基于 runtime 层的 hchan 结构体,内部包含环形缓冲区(用于有缓冲 channel)、发送 / 接收等待队列、锁(保护内部状态)等。
  • 同步特性:发送操作 (ch <- data):若 channel 已满(有缓冲)或无接收者(无缓冲),发送方 goroutine 会阻塞。接收操作 (data := <-ch):若 channel 为空,接收方 goroutine 会阻塞。

2. 常见类型与用法

(1)无缓冲 channel

  • 特点:缓冲区大小为 0,发送和接收必须同时就绪,否则阻塞。
  • 核心作用:用于 goroutine 之间的同步(确保两个 goroutine 在某个时间点 “握手”)。

(2)有缓冲 channel

  • 特点:缓冲区大小 > 0,发送方在缓冲区未满时可直接写入,接收方在缓冲区未空时可直接读取,无需阻塞。
  • 核心作用:用于异步通信限流(控制并发数)。

(3)单向 channel

  • 特点:只能发送 (chan<- T) 或只能接收 (<-chan T),用于在函数签名中约束 channel 的使用方向,提高代码安全性。

3. 与 select 结合:多路复用

select 可以同时监听多个 channel 的发送 / 接收操作,哪个先就绪就执行哪个。比如超时控制就可以用这个机制。

全部评论

相关推荐

04-03 11:52
郑州大学 Java
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务