首页
/ Folly SmallLocks 深度指南:用 MicroSpinLock 与 PicoSpinLock 实现字节级、比特级的细粒度锁

Folly SmallLocks 深度指南:用 MicroSpinLock 与 PicoSpinLock 实现字节级、比特级的细粒度锁

2026-09-09 14:22:19作者:胡易黎Nicole

folly/synchronization/SmallLocks.h 为 Meta(Facebook)开源的 C++ 库 folly 提供了两种内存占用极小、专为“高内存约束、低竞争概率”场景设计的互斥锁:单字节的 MicroSpinLock 与寄生在现有整数上、仅占一个比特位的 PicoSpinLock。本文以 SmallLocks.md 为骨架,结合 SmallLocks.h、两个锁的完整实现与测试代码,讲解它们的设计动机、API 用法、底层原理与适用边界,帮助你在大规模数据结构的每条记录上实现零额外内存成本的细粒度并发保护。

一、为什么需要"小到极致"的锁

在服务端大规模数据结构的场景下,内存往往是稀缺资源。设想一个拥有数百万甚至数十亿条记录的巨型哈希表或链表,如果为每条记录都配一个 std::mutex(通常数十字节),内存开销会完全失控;但如果整表共用一把大锁,又会带来严重的竞争。folly 的解决思路是:让锁小到可以"塞进"每条记录已有的空闲位——很多记录里恰好有一个闲置的字节甚至一个闲置的比特位,此时加锁可以做到几乎零额外内存成本。

这正是 SmallLocks 模块的定位:它不是通用互斥锁的替代品,而是为"内存极度受限、且竞争概率很低"的场景专门设计的锁。文档开篇就明确了这一适用前提,SmallLocks.h 头文件注释也强调:这些锁适合临界区极小、竞争不激烈的场景。

注意:该模块目前仅支持 x64 架构(文档原话 "This module is currently x64 only."),在移植到其他架构前需要自行评估。

二、两种锁的定位与取舍

SmallLocks.h 向外导出了两种锁,它们解决的是略有不同的"小":

锁类型 最小内存占用 工作原理 适用场景
MicroSpinLock 1 字节 独占整个字节,用 0/1 表示空闲/占用 记录里刚好有 1 个空闲字节可用
PicoSpinLock<T> 2/4/8 字节(1 个比特位) 在已有的整数类型上借用最高位做锁 记录里已有整数,且恰好有 1 个未用比特位

文档特别解释了为什么两种锁都要保留:x64 的 bts(bit test and set)指令无法直接作用在单个字节上,因此 MicroSpinLock 可以把体积压到 1 字节(sizeof(MicroSpinLock) 恰好为 1),而 PicoSpinLock 至少要寄生在 16 位及以上的整数上。这意味着:

  • 若你追求绝对最小体积(1 字节),选 MicroSpinLock
  • 若你手头已有带空闲位的整数,选 PicoSpinLock,可做到完全不增加内存。

两种锁都完整实现了 C++11 的 Lockable 概念lock / unlock / try_lock),因此可以直接配合 std::lock_guardstd::unique_lock 做 RAII 管理,无需额外包装。

三、MicroSpinLock:单字节自旋锁实战

3.1 定义与初始化

MicroSpinLock 定义在 MicroSpinLock.h,内部就是一个裸的 uint8_t lock_,没有构造函数——这是刻意为之:它必须保持 POD 类型,以便能放进 __attribute__((packed)) 之类的紧凑结构体中(gcc 不允许 packed 结构包含非 POD 成员)。

struct MicroSpinLock {
  enum { FREE = 0, LOCKED = 1 };
  uint8_t lock_;
  void init() noexcept;               // 置为 FREE,等价于零初始化
  bool try_lock() noexcept;           // 尝试获取,返回是否成功
  void lock() noexcept;               // 阻塞获取(自旋 + 休眠)
  void unlock() noexcept;             // 释放
};

初始化有两种等价方式:调用 init(),或直接零初始化——因为空闲状态被保证为全零比特,MicroSpinLock lock{0}; 即可直接使用。folly 自身的代码就是这么做的,例如 IOBuf.hMicroSpinLock observerListLock{0};

3.2 使用示例

#include <folly/synchronization/SmallLocks.h>
#include <mutex>

struct Record {
  uint64_t key;
  uint32_t value;
  MicroSpinLock lock;   // 1 字节,塞进记录不心疼
  Record() : lock() {}  // 零初始化即就绪,无需调用 init()
};

void update(Record& r, uint32_t v) {
  std::lock_guard<MicroSpinLock> g(r.lock);  // RAII,符合 Lockable 概念
  r.value = v;
}

配合 std::unique_lock 同样可行。此外 folly 还提供了别名 using MSLGuard = std::lock_guard<MicroSpinLock>;(见 MicroSpinLock.h),测试代码 SmallLocksTest.cpp 中的用法就是 MSLGuard g(v.lock);

3.3 底层实现原理

从源码看,MicroSpinLock 的获取与释放围绕"交换"这一原子操作展开:

  • try_lock():执行 xchg_acquire(LOCKED)——把字节原子地交换为 1,若交换回的值是 FREE(0) 则说明成功抢到锁。这里使用 std::atomic_exchange_explicit 并施加 memory_order_acquire,保证抢到锁后能看到临界区之前的写入。
  • lock():先尝试一次交换,失败后进入两级退避循环:外层反复交换,内层用 detail::Sleeper 等待——Sleeper 会先 pause 指令自旋、再 yield 让出时间片,避免无意义地烧 CPU。
  • unlock():用 memory_order_release 把字节存回 FREE,保证临界区内的写入在解锁前对其他线程可见。

锁状态被保证为全零(FREE = 0),这正是零初始化即可用的原因。该类型还带有 static_assert 强制其为标准布局且平凡类型(MicroSpinLock.h)。

3.4 附带福利:SpinLockArray(防伪共享的分片锁数组)

MicroSpinLock.h 还顺带导出了一个 SpinLockArray<T, N>:以 hardware_destructive_interference_size(通常 64 字节)为对齐与填充粒度,把 N 把锁排列成数组,每个锁独占一条缓存行,避免相邻分片锁之间发生伪共享(false sharing)。它适合"基于分片(shard)的加锁实现",且每个元素内部做了静态断言,保证锁不会跨缓存行。folly 的线程局部存储实现 ThreadLocalDetail.h 中即使用了 MicroSpinLock 相关机制。

四、PicoSpinLock:寄生在整数上的单比特锁

4.1 模板参数与初始化

PicoSpinLock 定义在 PicoSpinLock.h,是一个类模板:

template <class IntType, int Bit = sizeof(IntType) * 8 - 1>
struct PicoSpinLock { ... };
  • IntType:宿主整数类型,仅支持 16、32、64 位的有符号/无符号整型(源码有 static_assert 约束,小于 2 字节的类型无法使用)。
  • Bit:用作锁的比特位下标,默认取最高位(如 32 位整数的第 31 位),因此正常业务数据应保证不使用最高位。

它同样刻意没有构造函数以保持 POD 性,使用前二选一:调用 init(initialValue),或直接零初始化(此时等价于已解锁且 getData() == 0)。

4.2 核心 API

方法 语义
void init(IntType initialValue = 0) 初始化数据值并置为解锁态;initialValue 不得占用锁位
IntType getData() const 读取"其余位"的数据(锁位被掩掉),无需持锁即可安全调用
void setData(IntType w) 写入其余位数据,应在持锁时调用(除非能保证无并发)
bool try_lock() const 原子地把锁位从 0 置 1,成功返回 true
void lock() const 阻塞获取,内部用 Sleeper 退避等待
void unlock() const 原子地清除锁位,不动其余位

4.3 使用示例:借用整数的最高位

#include <folly/synchronization/SmallLocks.h>
#include <mutex>

struct CachedEntry {
  // 业务数据只用低 31 位,最高位留给锁
  uint32_t generation = 0;
  PicoSpinLock<uint32_t> lock;   // 寄生在同一个 4 字节上

  CachedEntry() { lock.init(0); }
};

void bump(CachedEntry& e) {
  std::lock_guard<PicoSpinLock<uint32_t>> g(e.lock);
  e.lock.setData(e.lock.getData() + 1);  // 持锁读写数据位
}

核心价值在于:锁和数据共用同一个整数对象,内存零开销。如果某个记录里恰好有一个整数的高位从未被使用,PicoSpinLock 就能把它变成一把完整的互斥锁。测试 SmallLocksTest.cpp 还验证了有符号类型(如 int16_t)场景:PicoSpinLock<int16_t, 0> 可以用 Bit = 0 借用最低位,且 getData()/setData() 对负数同样正确。

4.4 底层实现原理

MicroSpinLock 用"整字节交换"不同,PicoSpinLock 依赖单比特的原子位操作(这正是文档提到 x64 bts 指令的原因):

  • 获取:atomic_fetch_set(ref, Bit, memory_order_acquire)——原子地置位并返回旧值,若旧值为 0 说明抢锁成功(PicoSpinLock.h);
  • 释放:atomic_fetch_reset(ref, Bit, memory_order_release)——原子地清位并施加 release 屏障(PicoSpinLock.h);
  • 读写数据位:getData()load + 掩码setData()load→修改→store 保留锁位。

lock_ 成员还带有 alignas(atomic_ref<UIntType>::required_alignment) 对齐声明,保证原子位操作在目标平台上合法。由于 Bit 默认取最高位,init()/setData() 内部都有 FOLLY_SAFE_CHECK 拒绝侵占锁位的非法输入。

五、为什么两者不可互相替代

文档给出了两者并存的根本原因,值得展开:

  1. 指令集的限制:x64 的 bts(bit test and set)等位操作指令不能作用于单字节操作数,PicoSpinLock 只能寄生于 2 字节及以上的整数,因此其最小尺寸也大于 1 字节;
  2. 因此 sizeof(MicroSpinLock) 可以比 PicoSpinLock 更小:当记录里只有一个空闲字节、没有合适整数可用时,MicroSpinLock 是唯一选择。

测试 SmallLocksTest.cpp 用编译期断言锁定了这一点:

static_assert(sizeof(MicroSpinLock) == 1, "Size check failed");
// 打包结构:MicroSpinLock + int16_t = 3 字节
static_assert(sizeof(ignore1) == 3, "Size check failed");
// 打包结构:PicoSpinLock<uint32_t> + int16_t = 6 字节
static_assert(folly::kMscVer || sizeof(ignore2) == 6, "Size check failed");

两条 static_assert 分别验证了两种锁可以安全放入紧凑打包结构,且各自的最小体积符合预期。简言之:体积最小选 MicroSpinLock,零成本复用选 PicoSpinLock

六、与 C++11 Lockable 概念无缝衔接

两种锁都提供 lock() / unlock() / try_lock() 三件套,符合标准库对互斥量的要求,因此可以:

  • std::lock_guard / std::unique_lock 做 RAII 作用域管理(示例见上文,测试 SmallLocksTest.cpp 大量使用);
  • 直接用 std::unique_lock 配合条件变量等待(folly 内部也有此用法)。

这意味着把它们接入现有基于标准互斥量编写的泛型代码时几乎不需要改动——这也是"Lockable 概念"设计带来的直接收益。

七、重要提醒:优先考虑 MicroLock

两个头文件的文件头注释都放着一句醒目的忠告(MicroSpinLock.hPicoSpinLock.h):

N.B. You most likely do not want to use MicroSpinLock or any other kind of spinlock. Consider MicroLock instead.

原因是:在抢占式多任务操作系统里,用户态自旋锁有严重缺陷——等待线程反复轮询一个被阻塞线程持有的锁,纯属浪费时间片;让 OS 调度器把线程挂起睡眠,对系统响应性和吞吐量都更有利。自旋锁更适合内核态。因此 folly 提供了第三种选择:

  • MicroLockMicroLock.h):同样是 1 字节,但用 2 个比特位(bit0 = held 持有位,bit1 = wait 等待位)实现了"先自旋、后让出、再通过 folly::atomic_wait 真正睡眠"的渐进退避策略(慢路径见 MicroLock.cpp),并把剩余 6 个比特位开放给用户存数据(lockAndLoad / unlockAndStore / LockGuardWithData,还能与指针等对象做 union 复用其低位。
  • 它的模板参数 MaxSpinsMaxYields 可调节自旋与让出的次数,决定它"多像一把自旋锁"。

也就是说,如果允许付出 2 个比特位,MicroLock 通常是比两个自旋锁更稳妥的默认选择MicroSpinLockPicoSpinLock 则适合那些"只有 1 个比特/1 个字节、且竞争确实极低"的极端内存约束场景。建议在动手前先阅读 MicroLock.h 的完整文档注释,再按自己的负载做基准测试。

八、测试与性能数据佐证

8.1 正确性测试

SmallLocksTest.cpp 覆盖了:

  • 尺寸与打包:上文已展示的 sizeof 与 packed 结构静态断言;
  • 多线程压力测试SpinLockCorrectnessavailable_concurrency() * 2 个线程并发写数组并在持锁时校验一致性(L141-L155);另有基于 simpleStressTestlock/try_lock 压力测试(2 线程与硬件并发数两档);
  • PicoSpinLock 符号数据:验证有符号整数在持锁/未持锁下的 getData/setData
  • TSAN 死锁检测:开启 FOLLY_SANITIZE_THREAD 时验证错误加锁顺序会触发 "Cycle in lock order graph" 报告;
  • 寄存器破坏回归RegClobber 测试专门防止编译器寄存器分配导致的 try_lock 语义被破坏。

8.2 性能参考

基准程序 SmallLocksBenchmark.cpp 同时测量了无竞争与多线程竞争场景。源码中记录了历史运行数据(如 Intel Xeon E5-2680 v4 @ 2.40GHz 上的输出,L788-L1000):无竞争时 MicroSpinLockUncontendedBenchmark 约 10.95ns/次、PicoSpinLock<std::uint16_t> 约 20.38ns/次,明显快于 std::mutex 的 16.42ns/次;但在多线程高竞争(如 32 线程以上)下,自旋锁的公平性与吞吐会急剧恶化(最大等待时间可达数百毫秒),这印证了文档"竞争概率低"的前提——请务必用自己真实的负载重新基准

九、总结:如何选择

你的处境 推荐
能接受 1 字节额外空间,追求绝对最小锁 MicroSpinLock
记录里已有整数且高位空闲,想零内存成本加锁 PicoSpinLock<T>
愿意用 2 个比特位换取更好的等待策略 MicroLock(默认更推荐)
完全没有内存压力 std::mutex(更快、更成熟)

SmallLocks 家族的价值在于把"锁"的粒度细化到了字节乃至比特,让大规模数据结构的细粒度并发成为可能。入手源码时,建议按 SmallLocks.hMicroSpinLock.hPicoSpinLock.hMicroLock.h 的顺序阅读,再对照 SmallLocksTest.cppSmallLocksBenchmark.cpp 验证你的理解。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395