在高并发场景下,读操作通常远多于写操作。使用传统的互斥锁(std::mutex)会导致所有读线程被阻塞,从而浪费 CPU 资源。C++17 引入了 std::shared_mutex,它允许多个读线程同时访问共享资源,而写线程则独占访问。下面通过一个完整的示例来演示如何正确使用 std::shared_mutex,并讨论其性能与注意事项。
1. 基本使用
#include <shared_mutex>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>
class Cache {
public:
// 读取数据
int get(int key) const {
std::shared_lock lock(mutex_); // 共享锁
auto it = data_.find(key);
return it != data_.end() ? it->second : -1;
}
// 写入数据
void set(int key, int value) {
std::unique_lock lock(mutex_); // 独占锁
data_[key] = value;
}
private:
mutable std::shared_mutex mutex_;
std::unordered_map<int, int> data_;
};
std::shared_lock:在读取时使用,允许多个线程同时获取共享锁。std::unique_lock:在写入时使用,保证独占访问。
2. 读写分离的性能实验
int main() {
Cache cache;
constexpr int readers = 8;
constexpr int writers = 2;
// 写线程
std::vector<std::thread> writerThreads;
for (int i = 0; i < writers; ++i) {
writerThreads.emplace_back([&cache, i]() {
for (int j = 0; j < 1000; ++j) {
cache.set(i * 1000 + j, j);
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
});
}
// 读线程
std::vector<std::thread> readerThreads;
for (int i = 0; i < readers; ++i) {
readerThreads.emplace_back([&cache, i]() {
for (int j = 0; j < 5000; ++j) {
int val = cache.get(i);
(void)val;
std::this_thread::sleep_for(std::chrono::microseconds(5));
}
});
}
for (auto& t : writerThreads) t.join();
for (auto& t : readerThreads) t.join();
std::cout << "Benchmark finished.\n";
}
运行时可以看到,读线程几乎不会因为写线程的存在而被阻塞,从而显著提升了吞吐量。相比之下,使用 std::mutex 时,所有线程都必须排队,导致明显的性能下降。
3. 读写优先策略
std::shared_mutex 默认采用的是“读者优先”策略:当有读线程等待时,新来的读线程可以继续获得锁,而写线程只能等到所有读线程完成后才能获得独占锁。若想让写线程获得更高的优先级,可以使用 std::shared_timed_mutex 并在写操作前手动 try_lock_for 一段时间,或者直接使用 std::shared_mutex 并配合 std::unique_lock 的 lock() 来强制等待写锁。
// 写优先的写入
void set_with_priority(int key, int value) {
std::unique_lock lock(mutex_, std::defer_lock);
// 尝试在 100 毫秒内获取锁
if (!lock.try_lock_for(std::chrono::milliseconds(100))) {
// 如果超时,直接阻塞等待
lock.lock();
}
data_[key] = value;
}
4. 常见错误与陷阱
- 忘记加
mutable:std::shared_mutex成员在常成员函数中需要修改,因此声明为mutable。 - 锁粒度过大:把锁加在过大的代码块中会降低并发度。最好只锁住真正需要共享资源访问的部分。
- 读写不平衡:若写操作占比过高,
std::shared_mutex的优势不明显,甚至比std::mutex更慢。此时考虑使用std::mutex或读写分离的设计模式。
5. 与 std::shared_ptr 结合使用
在某些场景下,读线程需要持久化对共享资源的引用。结合 std::shared_mutex 与 std::shared_ptr 可以实现安全的读写访问:
class SharedData {
public:
void update(int newVal) {
std::unique_lock lock(mutex_);
data_ = std::make_shared <Data>(newVal);
}
std::shared_ptr<const Data> snapshot() const {
std::shared_lock lock(mutex_);
return data_;
}
private:
mutable std::shared_mutex mutex_;
std::shared_ptr<const Data> data_;
};
这样读线程可以在获得共享锁后获取 std::shared_ptr 的拷贝,锁释放后仍能安全地访问数据。
6. 结语
std::shared_mutex 为现代 C++ 提供了一种轻量级且易用的读写锁实现,适用于读多写少的高并发场景。通过合理的锁粒度控制、读写优先策略以及与智能指针配合使用,可以大幅提升程序的并发性能。记住:锁是性能的敌人,也是安全的守护者,使用时务必保持精确与高效。