理解可重启序列:一种替代 Mutex 和 Atomics 的高性能方案

在追求极致性能的过程中,开发者在处理同步问题时经常会遇到瓶颈。传统的工具如 mutexesatomic operations 是确保多线程间数据完整性的标准做法,但它们也伴随着代价:开销。随着核心数量的增加,这些锁的竞争可能成为显著的瓶颈,导致 CPU 周期浪费和延迟增加。

可重启序列 (rseq) 代表了 Linux 内核处理临界区的一种范式转变。rseq 不再依赖笨重的锁定机制,而是允许程序执行一段短小的指令序列,如果发生抢占,内核可以安全地中断并重启该序列,从而有效地为用户空间内存更新提供了一种“事务性”方法。

可重启序列的工作原理

rseq 的核心设计是为了处理临界区内的抢占问题。在典型场景下,如果一个线程在持有锁时被抢占,其他线程可能会自旋或阻塞,从而浪费资源。通过使用可重启序列,内核可以感知到线程正在进入的临界区。

正如社区讨论中所提到的,该机制的工作方式如下:

"The way it works is you advise the kernel whenever your program enters a critical section of code that you don't want interrupted... The first assembly opcode should be a move instruction that sets the rseq_cs field. The last instruction needs to be the thing that makes the modification to your global data structure."

这一过程创建了一个微小的用户空间事务。用户空间应用程序与内核之间的通信是双向的,并通过共享内存进行,这消除了在临界区内进行昂贵系统调用的需求。如果内核抢占了该线程,或者该线程被迁移到另一个 CPU,内核会检测到 rseq_cs 字段已被设置,并自动将指令指针重置到序列的起始位置,从而确保操作相对于线程的执行是原子性地重启并完成的。

性能优势:超越 Atomics

关于 rseq 最具启发性的说法之一是,它甚至可以超越 CPU 内部的原子操作。虽然 atomicsmutexes 更快,但它们仍然需要在核心之间进行缓存行同步,这在高并发环境下可能非常昂贵。

通过使用 rseq,开发者可以潜在地从某些热点路径中移除 mutexesatomics。由于内核管理着重启逻辑,应用程序可以执行简单的 loadsstores。如果没有发生抢占,代码将以原始内存访问的速度运行。如果确实发生了抢占,代价仅仅是重启一段非常短的指令序列(通常为 10 条或更少),这比竞争锁的开销要高效得多。

实际实现与工具

虽然底层机制涉及汇编和共享内存结构,但对于大多数开发者来说,并不需要从头开始实现 rseq。生态系统已经发展到可以提供更高层级的抽象。

对于那些希望将其集成到项目中的人,librseq 库(由 rseq 实现者维护)为常见用例(如计数器和链表)提供了必要的辅助工具。这使得开发者能够获得可重启序列的性能收益,而无需为每个临界区编写自定义汇编。

注意事项与权衡

尽管性能有所提升,但 rseq 并不是万能灵药。它专门针对非常短的临界区进行了优化。如果序列过长,抢占的可能性就会增加,从而导致频繁的重启,并可能降低性能。

此外,一些开发者认为,rseq 的优势在具有海量核心数量的环境中最为显著。在应用程序开发者能够完全控制少量线程的场景下,rseq 与传统的线程局部存储或优化的 atomics 之间的差距可能会更小。然而,随着硬件持续向数百个核心扩展,能够随 CPU 核心而非仅仅随线程进行扩展的能力,就变成了一个关键优势。

结论

可重启序列通过将“原子性”的责任从硬件 (atomics) 或操作系统调度器 (mutexes) 转移到两者之间的协作关系中,提供了一种减少同步开销的高级方式。通过利用共享内存将临界区视为可重启的事务,Linux 为高性能计算领域实现真正的线性扩展提供了一条路径。

Sources