C++ std::shared_mutex 读写锁

在多线程编程中,常常需要协调大量线程对共享数据的访问。当程序中存在数量占优的读线程时(例如,监控系统、数据缓存等),使用传统的std::mutex会迫使所有读线程串行化,形成不必要的性能瓶颈。C++17 引入的 std::shared_mutex 为此提供了更优的解决方案。它通过共享读独占写的灵活模式,允许多个读线程并发执行,从而释放了巨大的性能潜力。

1. 问题场景

我们有一个全局变量 counter,100 个线程负责读取它的值,10 个线程负责写入,为了防止竞态,我们在读写时都加了 lock

#if 1
#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::high_resolution_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::high_resolution_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;
}

#endif
最终计数器值: 10
总执行时间: 1840 毫秒
总操作数量: 110 次

虽然程序是正确的,数据也是安全的,但性能非常差。原因在于:

  • 所有线程(包括只读线程)都必须等待锁
  • 即使多个线程只是读取数据,也不能同时执行
  • 造成了严重的串行化,100 个读线程几乎变成排队执行

这就导致了性能瓶颈,大量读线程的并发能力被锁机制白白浪费。

2. 解决思路

针对这种场景,C++17 引入了一个新的同步原语:std::shared_mutex(共享互斥量)它在语义上与普通 std::mutex 最大的区别是:

  • 写锁(独占锁):同一时刻只能有一个线程持有
  • 读锁(共享锁):多个线程可以同时持有

注意:写线程之间仍然互斥,即:读与读之间不冲突,读与写冲突,写与写冲突。

#if 1
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
#include <chrono>

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();
}

void demo()
{
    auto start_time = std::chrono::high_resolution_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::high_resolution_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;
}

#endif
最终计数器值: 10
总执行时间: 440 毫秒
总操作数量: 110 次

从结果可以看出:

  • 数据正确性保持一致
  • 执行时间从 1840ms 降到 440ms,性能提升接近 4 倍
  • 主要原因是读线程得以并发执行,大大降低了锁竞争

3. 锁的开销

对于 std::shared_mutex 而言,其性能瓶颈与 std::mutex 类似:当线程等待锁时会阻塞,锁释放后需要唤醒等待线程,这一过程涉及用户态与内核态切换,以及寄存器状态的保存与恢复,单次开销较高。

std::shared_mutex 通过区分读操作写操作,允许多个线程同时持有读锁,从而减少不必要的阻塞。为了实现这一点,std::shared_mutex 内部需要维护无锁、读锁和写锁等状态,并基于这些状态的进行较为复杂的加锁、解锁操作,因此,加锁或释放锁的基础开销通常比互斥锁更高。

在使用 std::shared_mutex 时,一方面会减少成本,一方面也会提高成本,所以,我们需要平衡以下两点:

  1. 加锁、解锁的基础开销
  2. 线程阻塞导致的上下文切换开销

当读操作占多数、读操作快速完成、线程竞争激烈时,std::shared_mutex 能够通过降低上下文切换开销体现性能优势。相反,如果写操作频繁、竞争不激烈,读写锁的额外基础开销可能抵消其优势,甚至比互斥锁更慢。

此外,std::shared_mutex 的使用复杂度也高于 std::mutex,需要正确管理 std::shared_mutex 的获取与释放。

因此,std::shared_mutex 的性能优势主要体现在读多写少、读操作快速完成、竞争激烈的场景下,而在其他情况下,互斥锁往往更加高效且实现简单。

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

5 + 4 =