当前位置: 首页 > news >正文

深入 Go 底层原理(八):sync 包的实现剖析

1. 引言

除了 channel,Go 还提供了传统的、基于共享内存的同步原语,它们都位于 sync 包中。在高并发场景下,正确且高效地使用这些工具至关重要。理解它们的底层实现,能帮助我们避免死锁、竞争条件,并写出性能更优的并发代码。

本文将深入 sync 包,重点剖析 MutexWaitGroupPool 这三个最常用工具的内部工作原理。

2. sync.Mutex (互斥锁)

Mutex 用于保护临界区,确保同一时间只有一个 goroutine 可以访问共享资源。

核心数据结构:

type Mutex struct {state int32  // 锁的状态 (锁定、唤醒、饥饿等)sema  uint32 // 信号量,用于唤醒等待的 goroutine
}
  • state: 这是一个复合状态字段,通过位操作(bit manipulation)来存储多个信息:

    • Locked Bit: 标记锁是否已被持有。

    • Woken Bit: 标记是否有 goroutine 已被唤醒。

    • Starving Bit: 标记锁是否进入“饥饿”模式。

    • Waiter Count: 记录等待锁的 goroutine 数量。

工作模式: Mutex 有两种工作模式:

  1. 正常模式 (Normal Mode):

    • 当一个 goroutine 请求锁时,如果锁未被持有,它会立即获得锁。

    • 如果锁已被持有,它会通过几次**自旋(Spinning)**尝试获取锁。自旋是指在不挂起 goroutine 的情况下,进行忙等待循环。这对于锁很快会被释放的场景能提升性能。

    • 如果自旋后仍未获取到锁,goroutine 会被加入等待队列并挂起。

    • 当锁被释放时,会唤醒等待队列中的一个 goroutine。被唤醒的 goroutine 和新来的 goroutine 会公平竞争。

  2. 饥饿模式 (Starving Mode):

    • 如果一个 goroutine 等待锁的时间超过了 1ms,Mutex 会切换到饥饿模式

    • 在饥饿模式下,锁的所有权会直接交接给等待队列中的队头 goroutine,新来的 goroutine 即使自旋也无法获取锁。

    • 目的:防止等待队列中的 goroutine 因为不断有新来的“插队者”而长时间“饿死”,保证了公平性。

3. sync.WaitGroup

WaitGroup 用于等待一组 goroutine 全部执行完毕。

核心数据结构:

type WaitGroup struct {noCopy noCopy // 保证 WaitGroup 不被复制state1 [3]uint32
}
  • state1: 同样是一个复合字段,存储了两个核心信息:

    • Counter: 一个 32 位的计数器,记录需要等待的 goroutine 数量。

    • Waiter Count: 等待 Wait() 的 goroutine 数量。

工作原理:

  • Add(delta int): 直接通过原子操作(atomic operations)给 Counter 增加 delta

  • Done(): 等价于 Add(-1),通过原子操作将 Counter 减一。当 Counter 变为 0 时,它会唤醒所有在 Wait() 处等待的 goroutine。

  • Wait():

    • 检查 Counter 是否为 0。如果是,立即返回。

    • 如果不为 0,它会通过信号量机制将当前 goroutine 挂起,直到 Counter 变为 0 时被 Done() 唤醒。

4. sync.Pool (临时对象池)

Pool 用于缓存和复用临时对象,以减轻 GC 压力。它特别适用于那些需要频繁创建和销毁、生命周期短暂的对象的场景。

核心设计:

  • Pool 是并发安全的

  • Pool 中的对象随时可能被 GC 回收。你不应该依赖 Pool 来存储有状态的、持久的对象。

  • 每个 P (Processor) 都有自己的本地池:为了避免锁竞争,sync.Pool 为每个 P 都维护了一个私有的对象列表(poolLocal)。

工作原理:

  • Get():

    1. 优先尝试从当前 P 的本地私有池 (private) 中获取对象。此操作无锁。

    2. 如果私有池为空,尝试从当前 P 的本地共享池 (shared) 中获取。此操作也无锁。

    3. 如果本地共享池也为空,尝试从其他 P 的共享池中窃取一个对象。

    4. 如果窃取也失败,最后会调用 Pool 中预设的 New 函数(如果存在)来创建一个新对象。

  • Put(x interface{}):

    • 将对象 x 放入当前 P 的本地私有池中。此操作无锁。

GC 与 Pool:

  • 在每次 GC 开始时,sync.Pool 会清理其缓存。它会移除所有 P 的本地共享池中的对象,并将它们移动到一个“受害者缓存”中。私有池中的对象则会被直接丢弃。这保证了 Pool 不会无限增长,也解释了为什么池中的对象是不稳定的。

http://www.dtcms.com/a/310949.html

相关文章:

  • Node.js 操作 MongoDB
  • 【机器学习】“回归“算法模型的三个评估指标:MAE(衡量预测准确性)、MSE(放大大误差)、R²(说明模型解释能力)
  • 分布式事务----spring操作多个数据库,事务以及事务回滚还有用吗
  • Oracle 11gR2 Clusterware应知应会
  • 【unity组件_Transform 】
  • 设计模式篇:在前端,我们如何“重构”观察者、策略和装饰器模式
  • 蓝桥杯----串口
  • 内存、硬盘与缓存的技术原理及特性解析
  • 《软件测试与质量控制》实验报告二 单元测试
  • Ubuntu系统VScode实现opencv(c++)视频及摄像头使用
  • 空间平面旋转与xoy平行
  • 【BTC】挖矿
  • MyBatisPlus之CRUD接口(IService与BaseMapper)
  • 【软考中级网络工程师】知识点之堆叠
  • 公网服务器上Nginx或者Openresty如何屏蔽IP直接扫描
  • CS课程项目设计7:基于Canvas交互友好的五子棋游戏
  • 小智服务器Java安装编译(xinnan-tech)版
  • 【05】OpenCV C#——OpenCvSharp 图像基本操作---转灰度图、边缘提取、兴趣区域ROI,图像叠加
  • 28Rsync免密传输与定时备份
  • 【Spring Boot 快速入门】五、文件上传
  • 图漾相机-ROS1_SDK_ubuntu 4.X.X版本编译
  • Shell【脚本 02】离线安装配置Zookeeper及Kafka并添加service服务和开机启动(脚本分析)
  • [硬件电路-122]:模拟电路 - 信号处理电路 - 模拟电路与数字电路、各自的面临的难题对比?
  • [硬件电路-124]:模拟电路 - 信号处理电路 - 测量系统的前端电路详解
  • 编程与数学 03-002 计算机网络 20_计算机网络课程实验与实践
  • filezilla出现connected refused的时候排查问题
  • Flink2.0学习笔记:Stream API 窗口
  • 鸿蒙智选携手IAM进驻长隆熊猫村,为国宝打造智慧健康呼吸新空间
  • 智能合约漏洞导致的损失,法律责任应如何分配
  • Hyperliquid:揭秘高性能区块链共识引擎HyperBFT