多线程之读写锁

多线程之读写锁
苏丙榅1. 读写锁
1.1 读写锁模型
在多线程编程中,普通的 std::mutex 是独占锁。同一时间,只允许一个线程访问数据。然而,对于读多写少的数据(例如配置文件、缓存数据),这是低效的,因为读取操作本身是线程安全的,只需要防止在读取的过程中被写入打断即可。
std::shared_timed_mutex 和 std::shared_lock 是 C++14 引入的一对组合,用于实现读写锁机制。读写锁机制允许通过区分读操作和写操作来提高程序的并发性能。其核心思想是:读操作是共享的,写操作是独占的。
- 独占模式:
- 对应 写操作。
- 如果一个线程获得了独占锁(写锁),其他任何线程都无法获得共享锁(读)或独占锁(写)。
- 配合类:通常使用
std::unique_lock。
- 共享模式:
- 对应 读操作。
- 如果一个线程获得了共享锁(读锁),其他线程可以同时获得共享锁来读取数据。
- 但是,其他线程无法获得独占锁(写),直到所有共享锁被释放。
- 配合类:必须使用
std::shared_lock。
| 操作类型 | 线程 A (读) | 线程 B (读) | 线程 C (写) |
|---|---|---|---|
| 场景 1 | ✅ 可以进入 | ✅ 可以进入 | ❌ 必须等待 |
| 场景 2 | ⛔ 等待 | ⛔ 等待 | ✅ 正在写入 |
1.2 类详解
std::shared_timed_mutex
std::shared_timed_mutex 在线 API 文档这是一个互斥量类,类似于
std::mutex,位于<shared_mutex>头文件中。支持共享(读)和独占(写)两种锁定模式,带有超时功能。主要成员函数:
构造函数:构造互斥量,调用后互斥量处于未锁定状态。
1
shared_timed_mutex();
独占锁定
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 锁住互斥量。如果其他线程已锁住互斥量,则对 lock 的调用将阻塞执行,直到获得锁。
void lock();
// 尝试锁定互斥量。立即返回。成功获取锁时返回 true,否则返回 false。
bool try_lock();
// 试锁定互斥量。阻塞直到指定的持续时间 timeout_duration 已过去(超时)或锁被获取(拥有互斥量),
// 以先发生者为准。成功获取锁时返回 true,否则返回 false。
template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep, Period>& timeout_duration );
// 尝试锁定互斥量。阻塞直到达到指定的时间点 timeout_time(超时)或成功获取锁(拥有互斥量),
// 以先发生者为准。成功获取锁时返回 true,否则返回 false。
template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock, Duration>& timeout_time );
// 解锁互斥量
void unlock();共享锁定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 获取互斥量的共享所有权。如果另一个线程以独占所有权持有互斥量,则调用 lock_shared
// 将会阻塞执行,直到可以获取共享所有权。
void lock_shared();
// 尝试以共享模式锁定互斥体。立即返回。如果成功获取锁,则返回true,否则返回false。
bool try_lock_shared();
// 尝试以共享模式锁定互斥量。阻塞直到指定的 timeout_duration 已过去或者共享锁被获取,以先发生者为准。
// 成功获取锁时返回 true,否则返回 false。
template< class Rep, class Period >
bool try_lock_shared_for( const std::chrono::duration<Rep,Period>& timeout_duration );
// 尝试以共享模式锁定互斥量。阻塞直到达到指定的时间点 timeout_time 或获得锁,以先发生者为准。
// 成功获得锁时返回 true,否则返回 false。
template< class Clock, class Duration >
bool try_lock_shared_until( const std::chrono::time_point<Clock,Duration>& timeout_time );
// 释放调用线程对互斥体的共享所有权。
void unlock_shared();
std::shared_lock
std::shared_lock 在线 API 文档这是一个RAII 风格的锁包装器,类似于
std::unique_lock,但它是专门为配合shared_timed_mutex(或shared_mutex)的共享模式设计的。当构造一个
std::shared_lock对象时,它会自动调用互斥量的lock_shared();当std::shared_lock对象析构时,它会自动调用unlock_shared()。这意味着如果代码抛出异常,锁也能被正确释放,避免死锁。RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 中的一个核心编程范式,它利用对象的生命周期来管理资源(如内存、文件句柄、互斥锁等),确保资源在使用后被正确释放,从而避免资源泄漏。
构造函数:第一个参数:
std::shared_timed_mutex对象。1
2
3
4explicit shared_lock( mutex_type& m );
shared_lock( mutex_type& m, std::defer_lock_t t ) noexcept;
shared_lock( mutex_type& m, std::try_to_lock_t t );
shared_lock( mutex_type& m, std::adopt_lock_t t );第二个参数参数类型 效果 实参 defer_lock_t不获取互斥量所有权 std::defer_locktry_to_lock_t尝试获取互斥量所有权而不阻塞 std::try_to_lockadopt_lock_t假定调用线程已拥有互斥量 std::adopt_lock
2. 读写锁的使用
下面的代码模拟了一个简单的共享数据缓存。我们将看到:
- 多个读者线程:使用
std::shared_lock同时读取数据。 - 一个作者线程:使用
std::unique_lock定期修改数据。 - 超时尝试:演示如何尝试带超时的读取。
1 |
|
代码运行逻辑解析:
- 初始阶段 (0ms - 800ms):
- 3 个
Reader线程启动。 - 因为没有
Writer,它们都可以通过std::shared_lock获取共享锁。 - 现象:会看到多个
[Reader]正在读取数据几乎同时打印出来。这正是共享锁的魅力:并发读取。
- 3 个
- 写者介入 (800ms):
Writer线程醒来,调用update。- 它尝试获取
unique_lock(独占锁)。 - 此时,可能还有读者持有锁。Writer 必须等待所有的
shared_lock释放。
- 阻塞阶段:
- 一旦 Writer 成功获取锁,所有新的
read()请求都会阻塞。 - 如果此时
timeout_reader尝试读取并限制 200ms,它无法获得锁,因此打印[TryReader] 读取失败:等待超时!。这演示了std::shared_timed_mutex的超时控制能力。
- 一旦 Writer 成功获取锁,所有新的
- 写入阶段:
- 持有锁 1000ms(模拟慢速写入),期间阻塞所有其他访问。
- 打印
[Writer] 数据更新为…。
- 恢复阶段:
Writer完成,锁释放。- 被阻塞的读者线程再次苏醒,继续并发读取数据。
总结与建议:
- 何时使用
std::shared_lock:当你访问共享数据且不打算修改它时。 - 何时使用
std::unique_lock:当你需要修改共享数据时。 - 何时使用
std::shared_timed_mutex:当你需要读写分离,并且可能在获取锁时有超时需求(例如在日志系统、异步任务队列中很常见)。
如果不需要超时功能(即普通的 lock 和 unlock),编译器优化的 std::shared_mutex (C++17引入) 在某些平台上性能可能更好。但在 C++14 环境下,std::shared_timed_mutex 是唯一的选择。

















