C++ 高性能并发编程:基于 RAII 与 CAS 的轻量级原子读写锁设计
一种基于 C++ 模板元编程和 std::atomic 实现的高性能原子读写锁。针对传统系统调用锁在低延迟场景下的性能瓶颈,采用分层设计将锁管理与实现解耦。核心利用单变量状态机配合 CAS 指令实现无锁化,通过内存序控制优化指令重排,并引入写优先策略解决饥饿问题。结合 RAII 风格的 Guard 类确保资源安全释放,最终提供了一套防死锁、零成本抽象且支持写优先的轻量级同步原语方案。

一种基于 C++ 模板元编程和 std::atomic 实现的高性能原子读写锁。针对传统系统调用锁在低延迟场景下的性能瓶颈,采用分层设计将锁管理与实现解耦。核心利用单变量状态机配合 CAS 指令实现无锁化,通过内存序控制优化指令重排,并引入写优先策略解决饥饿问题。结合 RAII 风格的 Guard 类确保资源安全释放,最终提供了一套防死锁、零成本抽象且支持写优先的轻量级同步原语方案。

在 C++ 中,手动调用 lock() 和 unlock() 是非常危险的行为。一旦临界区内发生异常,或者开发者在某个分支提前 return 而忘记解锁,就会导致死锁。
为了解决这个问题,我们首先实现一个 RAII(Resource Acquisition Is Initialization)层。它的核心思想是:利用局部对象的生命周期来管理资源,在构造时加锁,在析构时解锁。
我们通过模板类来实现 ReadLockGuard 和 WriteLockGuard:
// rw_lock_guard.h
namespace cmw {
namespace base {
/**
* @brief 读锁的 RAII 封装
* @tparam RWLock 读写锁类型,需要提供 ReadLock() 和 ReadUnlock() 接口
*/
template<typename RWLock>
class ReadLockGuard {
public:
explicit ReadLockGuard(RWLock& rw_lock) : rw_lock_(rw_lock) {
rw_lock_.ReadLock(); // 构造时加读锁
}
~ReadLockGuard() {
rw_lock_.ReadUnlock(); // 析构时解读锁
}
private:
// 禁用拷贝和移动语义,防止锁的拥有权被意外转移
ReadLockGuard(const ReadLockGuard&) = delete;
ReadLockGuard& operator=(const ReadLockGuard&) = delete;
RWLock& rw_lock_;
};
/**
* @brief 写锁的 RAII 封装
* @tparam RWLock 读写锁类型,需要提供 WriteLock() 和 WriteUnlock() 接口
*/
template<typename RWLock>
class WriteLockGuard {
public:
explicit WriteLockGuard(RWLock& rw_lock) : rw_lock_(rw_lock) {
rw_lock_.WriteLock(); // 构造时加写锁
}
~WriteLockGuard() {
rw_lock_.WriteUnlock(); // 析构时解写锁
}
private:
WriteLockGuard(const WriteLockGuard&) = delete;
WriteLockGuard& operator=(const WriteLockGuard&) = delete;
RWLock& rw_lock_;
};
} // namespace base
} // namespace cmw
你可能会问,为什么不直接在锁的类里面写死这些逻辑?这种基于模板的泛型分层设计有三大绝佳优势:
有了安全的 RAII 外壳,接下来我们来实现硬核的底层锁:AtomicRWLock。
传统的 std::mutex 或 std::shared_mutex 底层通常依赖操作系统的同步原语(如 Linux 的 futex)。当锁竞争失败时,线程会被挂起(Sleep),这涉及用户态到内核态的切换以及线程上下文切换。这个过程的开销通常在微秒级(us)。
在极高并发或超低延迟(纳秒级,ns)的场景下,这种开销是不可接受的。因此,我们需要一种完全在用户态解决竞争的方案,这就是基于 std::atomic 的无锁化(Lock-free)设计。
在无锁编程中,最核心的限制是:CPU 的 CAS(Compare-And-Swap)指令通常只能保证对单个内存地址的原子操作。
因此,我们不能用一个变量表示'是否有写锁',再用另一个变量表示'有多少个读锁',因为你无法同时原子地修改这两个变量。
我们的解决方案是:将所有状态压缩到一个 std::atomic<int32_t> lock_num_ 中:
0:空闲状态(RW_LOCK_FREE)。-1:被写线程独占(WRITE_EXCLUSIVE)。> 0:正在被多个读线程共享,其数值代表当前持有读锁的线程数量。通过这种设计,无论是加读锁(+1)还是加写锁(变成 -1),都可以通过一次 CAS 操作完成,保证了绝对的原子性。
读写锁最容易遇到的坑是写饥饿:如果读线程源源不断地到来,lock_num_ 一直大于 0,写线程可能永远也拿不到锁。
为此,我们引入了辅助变量 write_lock_wait_num_。当写线程准备获取锁时,先将此变量 +1。读线程在尝试加锁前,必须先检查这个变量。如果发现有写线程在等,读线程就会主动放弃竞争,把机会让给写线程。
我们来看看加写锁的核心逻辑:
// 尝试将状态从 0 修改为 -1
while (!lock_num_.compare_exchange_weak(rw_lock_free, WRITE_EXCLUSIVE, ...)) {
rw_lock_free = RW_LOCK_FREE; // CAS 失败会修改 expected 值,需重置
if (++retry_times > MAX_RETRY_TIMES) {
retry_times = 0;
std::this_thread::yield(); // 避免死自旋占满 CPU
}
}
为什么用 compare_exchange_weak 而不是 strong?
在某些架构(如 ARM)上,weak 版本可能会出现'伪失败'(即使值相等也返回 false),但它的指令开销更小。因为我们本来就在一个 while 循环中,伪失败大不了再循环一次,所以用 weak 性能更高。
为什么要 yield?
如果单纯使用 while 死循环(纯自旋锁),当持有锁的线程被操作系统调度走时,等待锁的线程会 100% 占满 CPU 核心进行无意义的空转。引入 std::this_thread::yield() 后,如果重试多次仍未拿到锁,线程会主动告诉 OS:'我先放弃当前的时间片,你去执行别的线程吧'。这是一种极佳的性能折中。
在并发编程中,比死锁更可怕的是指令重排。编译器和 CPU 为了优化性能,可能会打乱代码的执行顺序。如果'读取共享数据'的指令被重排到了'获取锁'的指令之前,锁就形同虚设了。
因此,我们不能使用默认的 std::memory_order_seq_cst(全局顺序一致性,开销最大,会清空 CPU 流水线),而是精准地使用了 acquire-release 语义:
这种精细的内存序控制,能将 CPU 缓存一致性协议(如 MESI)的同步开销降到最低,榨干硬件的最后一丝性能。
为了让你更好地理解上述所有细节是如何协同工作的,这里给出 AtomicRWLock 的完整实现代码:
// atomic_rw_lock.h
#include <atomic>
#include <thread>
#include "rw_lock_guard.h"
namespace cmw {
namespace base {
class AtomicRWLock {
// 允许 Guard 类访问私有的加解锁接口,对外隐藏底层细节
friend class ReadLockGuard<AtomicRWLock>;
friend class WriteLockGuard<AtomicRWLock>;
public:
static constexpr int32_t RW_LOCK_FREE = 0;
static constexpr int32_t WRITE_EXCLUSIVE = -1;
static constexpr uint32_t MAX_RETRY_TIMES = 5;
AtomicRWLock() = default;
explicit AtomicRWLock(bool write_first) : write_first_(write_first) {}
private:
std::atomic<uint32_t> write_lock_wait_num_ = {0};
std::atomic<int32_t> lock_num_ = {0};
bool write_first_ = true;
void ReadLock() {
uint32_t retry_times = ;
lock_num = lock_num_.(std::memory_order_acquire);
(write_first_) {
{
(lock_num < RW_LOCK_FREE || write_lock_wait_num_.(std::memory_order_acquire) > ) {
(++retry_times > MAX_RETRY_TIMES) {
retry_times = ;
std::this_thread::();
}
lock_num = lock_num_.(std::memory_order_acquire);
}
} (!lock_num_.(lock_num, lock_num + , std::memory_order_acq_rel, std::memory_order_relaxed));
} {
{
(lock_num < RW_LOCK_FREE) {
(++retry_times > MAX_RETRY_TIMES) {
retry_times = ;
std::this_thread::();
}
lock_num = lock_num_.(std::memory_order_acquire);
}
} (!lock_num_.(lock_num, lock_num + , std::memory_order_acq_rel, std::memory_order_relaxed));
}
}
{
retry_times = ;
rw_lock_free = RW_LOCK_FREE;
write_lock_wait_num_.(, std::memory_order_release);
(!lock_num_.(rw_lock_free, WRITE_EXCLUSIVE, std::memory_order_acq_rel, std::memory_order_relaxed)) {
rw_lock_free = RW_LOCK_FREE;
(++retry_times > MAX_RETRY_TIMES) {
retry_times = ;
std::this_thread::();
}
}
write_lock_wait_num_.(, std::memory_order_release);
}
{
lock_num_.(, std::memory_order_release);
}
{
lock_num_.(, std::memory_order_release);
}
};
}
}
得益于分层设计,在业务代码中使用这套高并发组件变得异常简单且安全:
cmw::base::AtomicRWLock my_lock(true); // 开启写优先
int shared_data = 0;
void ReaderThread() {
// 作用域开始,自动加读锁
cmw::base::ReadLockGuard<cmw::base::AtomicRWLock> guard(my_lock);
std::cout << "Read data: " << shared_data << std::endl;
// 作用域结束,自动解读锁
}
void WriterThread() {
// 作用域开始,自动加写锁
cmw::base::WriteLockGuard<cmw::base::AtomicRWLock> guard(my_lock);
shared_data++; // 作用域结束,自动解写锁
}
通过这篇文章,我们实现了一个工业级的高性能读写锁。回顾整个过程,我们做对了三件事:
希望这种分层设计与底层优化的思路,能对你的日常开发有所启发!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online