C++ 读写锁(std::shared_mutex)用法

并发编程里,为了避免多个线程同时读写同一块数据而产生数据竞争,我们通常会加锁。传统的 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));
}

未经允许不得转载:一亩三分地 » C++ 读写锁(std::shared_mutex)用法
评论 (0)

2 + 4 =