并发编程里,为了避免多个线程同时读写同一块数据而产生数据竞争,我们通常会加锁。传统的 std::mutex 做法比较简单粗暴。无论读还是写,都只能让一个线程进去。当任务中存在读多写少的情况,这种一刀切的独占策略就会成为性能瓶颈。
为了解决这个问题,C++17 提供了 std::shared_mutex。它把对临界区的访问区分成读和写两类:读操作可以并发进行,写操作依然保持排他。这样一来,在读请求远多于写请求的场景下,可以充分释放并行读取的潜力,让整体吞吐量大幅提升。
1. 问题场景
读写锁在多读少写场景下能显著提升性能。接下来我们构造一个直观场景来验证这一点:假设有一个全局变量 counter,我们启动 100 个线程负责读取它的值,同时启动 10 个线程负责修改它,为了避免竞态条件,先用互斥锁对这个共享变量进行保护,后续可对比读写锁的性能差异。
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
std::mutex mtx;
int counter = 0;
const int READER_COUNT = 100;
const int WRITER_COUNT = 10;
void read_function()
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
mtx.lock();
int number = counter;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
mtx.unlock();
}
void write_function()
{
std::this_thread::sleep_for(std::chrono::milliseconds(2));
mtx.lock();
++counter;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
mtx.unlock();
}
void demo()
{
auto start_time = std::chrono::steady_clock::now();
std::vector<std::thread> readers;
std::vector<std::thread> writers;
// 创建读线程
for (int i = 0; i < READER_COUNT; ++i)
{
readers.emplace_back(read_function);
}
// 创建写线程
for (int i = 0; i < WRITER_COUNT; ++i)
{
writers.emplace_back(write_function);
}
// 等待线程执行完毕
for (auto& reader : readers)
{
reader.join();
}
for (auto& writer : writers)
{
writer.join();
}
auto end_time = std::chrono::steady_clock::now();
auto time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
std::cout << "最终计数器值: " << counter << std::endl;
std::cout << "总执行时间: " << time.count() << " 毫秒" << std::endl;
std::cout << "总操作数量: " << (READER_COUNT + WRITER_COUNT) << " 次" << std::endl;
}
int main()
{
demo();
return 0;
}
最终计数器值: 10 总执行时间: 1748 毫秒 总操作数量: 110 次
虽然程序是正确的,数据也是安全的,但性能非常差。原因在于:
- 所有线程(包括只读线程)都必须等待锁
- 即使多个线程只是读取数据,也不能同时执行
- 造成了严重的串行化,100 个读线程几乎变成排队执行
这就导致了性能瓶颈,大量读线程的并发能力被锁机制白白浪费。
2. 解决方法
在这种场景下,更合适的做法是把普通互斥锁换成读写锁。读写锁允许多个读线程并发进入临界区,但在写操作到来时又能保证排他性。C++ 的读写锁(std::shared_mutex)遵循以下并发规则:
- 读读并行:多个线程可以同时获取共享锁并执行读操作。
- 读写互斥:有线程在读时,写线程无法获得锁,有线程在写时,读线程也无法获得锁。
- 写写互斥:写操作需要独占锁,多个写线程不能同时进入。
#include <shared_mutex>
// std::mutex mtx;
std::shared_mutex mtx;
int counter = 0;
const int READER_COUNT = 100;
const int WRITER_COUNT = 10;
void read_function()
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
// 读锁加锁
mtx.lock_shared();
int number = counter;
std::this_thread::sleep_for(std::chrono::milliseconds(2));
// 读锁解锁
mtx.unlock_shared();
}
void write_function()
{
std::this_thread::sleep_for(std::chrono::milliseconds(2));
// 写锁加锁
mtx.lock();
++counter;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// 写锁解锁
mtx.unlock();
}
最终计数器值: 10 总执行时间: 333 毫秒 总操作数量: 110 次
从结果来看,把读操作放宽为可并行执行后,整体时间从 1748ms 缩减到 333ms,性能提升非常明显。
前面的示例里,我们是手动控制读写锁的获取与释放。在实际项目中,更常见的做法是配合 RAII 风格的锁管理工具,让读写锁的生命周期自动管理。示例代码如下:
void read_function()
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
// 读锁自动管理
std::shared_lock<std::shared_mutex> lock(mtx);
int number = counter;
std::this_thread::sleep_for(std::chrono::milliseconds(2));}
void write_function()
{
std::this_thread::sleep_for(std::chrono::milliseconds(2));
// 写锁自动管理
std::unique_lock<std::shared_mutex> lock(mtx);
++counter;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}

冀公网安备13050302001966号