如何使用 std::shared_mutex 实现高并发读写锁

在高并发场景下,读操作通常远多于写操作。使用传统的互斥锁(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_locklock() 来强制等待写锁。

// 写优先的写入
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. 常见错误与陷阱

  1. 忘记加 mutablestd::shared_mutex 成员在常成员函数中需要修改,因此声明为 mutable
  2. 锁粒度过大:把锁加在过大的代码块中会降低并发度。最好只锁住真正需要共享资源访问的部分。
  3. 读写不平衡:若写操作占比过高,std::shared_mutex 的优势不明显,甚至比 std::mutex 更慢。此时考虑使用 std::mutex 或读写分离的设计模式。

5. 与 std::shared_ptr 结合使用

在某些场景下,读线程需要持久化对共享资源的引用。结合 std::shared_mutexstd::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++ 提供了一种轻量级且易用的读写锁实现,适用于读多写少的高并发场景。通过合理的锁粒度控制、读写优先策略以及与智能指针配合使用,可以大幅提升程序的并发性能。记住:锁是性能的敌人,也是安全的守护者,使用时务必保持精确与高效。

发表评论