c++读写锁

好的,我们来详细解释一下 C++ 中的读写锁 (Read-Write Lock)。

1. 什么是读写锁?

读写锁,顾名思义,是一种将"读"和"写"两种操作区分开来对待的同步原语。它允许多个线程同时进行读操作 ,但只允许一个线程进行写操作

为了更好地理解,我们可以将其与标准的互斥锁 (std::mutex) 进行对比:

  • 互斥锁 (std::mutex):非常"霸道"。无论线程是想读还是想写,只要一个线程锁定了资源,其他任何线程都必须等待,不管它们想做什么。这就像一个只有一个座位的房间,一次只能进去一个人。
  • 读写锁 (std::shared_mutex) :更加"智能"和高效。它遵循以下规则:
    • 读-读共享:当没有线程在进行写操作时,任意数量的线程都可以同时获取读锁并访问资源。
    • 写-写互斥:同一时间只能有一个线程获取写锁。
    • 读-写互斥:当一个线程持有写锁时,其他任何线程(无论是想读还是想写)都必须等待。反之,当至少有一个线程持有读锁时,任何想获取写锁的线程都必须等待。

这就像一个图书馆的阅览室:可以有很多读者同时在里面看书(并发读),但如果有人要更换或整理所有书籍(写操作),那么所有读者都必须离开,并且在整理完成前,谁也不能进来。

2. 为什么要使用读写锁?

核心优势在于性能。

在"读多写少"的场景下,读写锁的性能远超于互斥锁。

想象一个场景:一个在线服务的配置数据。这个配置在服务启动时加载,并且很少会被修改。但是,成千上万个并发请求需要频繁地读取这些配置。

  • 如果使用互斥锁:每个请求在读取配置时都需要加锁,导致所有读请求变成了串行操作,严重影响并发性能,形成性能瓶颈。
  • 如果使用读写锁:所有读请求可以同时、并发地进行,几乎没有等待。只有在管理员需要更新配置(写操作)时,读请求才需要短暂等待。一旦写操作完成,大量的读请求又可以并发执行。

因此,当你的共享资源被读取的频率远高于被修改的频率时,读写锁是提升程序并发能力的神器。

3. C++中的读写锁实现

C++17 标准正式引入了 std::shared_mutex 作为读写锁的实现。它位于 <shared_mutex> 头文件中。在 C++14 中,有一个功能类似的 std::shared_timed_mutex。我们主要关注 C++17 的版本。

std::shared_mutex 配合使用的通常是两种 RAII 风格的锁管理器:

  1. std::shared_lock<std::shared_mutex> :用于获取读锁(共享锁)

    • 在其构造函数中调用 shared_mutexlock_shared() 方法。
    • 在其析构函数(离开作用域时)中调用 unlock_shared() 方法。
  2. std::unique_lock<std::shared_mutex>std::lock_guard<std::shared_mutex> :用于获取写锁(独占锁)

    • 在其构造函数中调用 shared_mutexlock() 方法。
    • 在其析构函数(离开作用域时)中调用 unlock() 方法。

使用 RAII 风格的锁(shared_lock, lock_guard)是最佳实践,因为它们可以确保即使在发生异常时,锁也能被正确释放,从而避免死锁。

4. 代码示例

下面是一个简单的例子,模拟一个被多个"读者"和一个"作者"线程共享的电话簿。

C++

复制代码
#include <iostream>
#include <map>
#include <string>
#include <thread>
#include <vector>
#include <shared_mutex> // 引入读写锁头文件
#include <chrono>

// 共享资源:一个电话簿
std::map<std::string, int> tele_book;
// 创建一个读写锁实例
std::shared_mutex tele_book_mutex;

// 读者线程函数
void reader(int id) {
    for (int i = 0; i < 5; ++i) {
        // 使用 shared_lock 获取读锁
        std::shared_lock<std::shared_mutex> lock(tele_book_mutex);
        // 在持有读锁期间,其他读者也可以进入,但作者必须等待
        std::cout << "Reader " << id << " is reading... ";
        auto it = tele_book.find("Alice");
        if (it != tele_book.end()) {
            std::cout << "Found Alice's number: " << it->second << std::endl;
        } else {
            std::cout << "Could not find Alice's number." << std::endl;
        }
        // 模拟读取耗时
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        // 当 lock 离开作用域时,读锁会自动释放
    }
}

// 作者线程函数
void writer() {
    for (int i = 0; i < 2; ++i) {
        // 模拟写入前的准备工作
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        
        // 使用 lock_guard (或 unique_lock) 获取写锁
        std::lock_guard<std::shared_mutex> lock(tele_book_mutex);
        // 在持有写锁期间,任何其他线程(读者或作者)都必须等待
        std::cout << "\nWriter is updating the book...\n" << std::endl;
        tele_book["Alice"] = 100 + i;
        tele_book["Bob"] = 200 + i;
        
        // 模拟写入耗时
        std::this_thread::sleep_for(std::chrono::seconds(1));
        // 当 lock 离开作用域时,写锁会自动释放
    }
}

int main() {
    std::vector<std::thread> threads;

    // 创建一个作者线程
    threads.emplace_back(writer);

    // 创建三个读者线程
    threads.emplace_back(reader, 1);
    threads.emplace_back(reader, 2);
    threads.emplace_back(reader, 3);

    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

代码解读与输出分析:

  1. 程序开始时,三个读者线程会几乎同时获取到读锁,并开始读取数据。你会看到类似 "Reader 1 is reading...", "Reader 2 is reading..." 交错出现。
  2. 当作者线程尝试获取写锁时,它必须等待所有当前持有读锁的读者线程完成并释放锁。
  3. 一旦所有读者都释放了锁,作者线程就会获得写锁,并打印 "Writer is updating..."。在此期间,所有读者线程的新一轮读取都会被阻塞,等待作者完成。
  4. 作者释放写锁后,读者们又可以一拥而上,并发地进行读取。
  5. 这个过程会重复进行。

5. 注意事项与潜在问题

  • 写者饥饿 (Writer Starvation) :这是一个经典问题。如果读请求非常密集,接连不断地到来,那么写者线程可能永远也等不到所有读者都释放锁的那个"空窗期",从而导致"饥饿"。C++标准本身没有规定 std::shared_mutex 必须如何解决这个问题,其具体行为(例如,是优先读者还是优先作者)取决于编译器的实现。一些实现可能会在有写者等待时,阻止新的读者获取锁,以避免写者饥饿。
  • 性能开销:读写锁本身比普通互斥锁更复杂,管理和调度的开销也更大。因此,在"读多写少"不明显的场景(例如,读写比例接近或写操作更多),使用读写锁可能反而会降低性能。请根据实际场景进行性能测试和选择。
  • 死锁:和所有锁一样,不当使用也会导致死锁。例如,一个线程持有了读锁,又尝试去获取写锁,这在大多数实现上都会导致死锁。正确的做法是先释放读锁,再去申请写锁。

总结

|------------|---------------------------------------|----------------------------------------------------------------------------|
| 特性 | 互斥锁 (std::mutex) | 读写锁 (std::shared_mutex) |
| 核心机制 | 一次只允许一个线程访问 | 允许多个读者一个作者访问 |
| 主要锁管理器 | std::lock_guard, std::unique_lock | 读锁: std::shared_lock &lt;br> 写锁: std::lock_guard, std::unique_lock |
| 最佳适用场景 | 读写操作频率相当,或写操作频繁 | 读操作远多于写操作 |
| 潜在问题 | 简单,但可能在读密集场景下成为性能瓶颈 | 更复杂,可能有写者饥饿问题,自身开销略高 |

当你确定你的应用场景是典型的"读多写少"时,std::shared_mutex 是一个非常值得使用的工具,它能显著地提升你程序的并发性能。

相关推荐
Jo乔戈里7 分钟前
计量经济学(复习/自用/未完)
算法
苦学LCP的小猪11 分钟前
LeeCode94二叉树的中序遍历
数据结构·python·算法·leetcode
实习生小黄13 分钟前
基于扫描算法获取psd图层轮廓
前端·javascript·算法
CYRUS_STUDIO1 小时前
破解 VMP+OLLVM 混淆:通过 Hook jstring 快速定位加密算法入口
android·算法·逆向
?abc!2 小时前
(哈希)128. 最长连续序列
算法·leetcode·哈希算法
Zephyrtoria3 小时前
动态规划:01 背包(闫氏DP分析法)
java·算法·动态规划
范纹杉想快点毕业3 小时前
解析Qt文件保存功能实现
java·开发语言·c++·算法·命令模式
Uyker3 小时前
前端与后端主流框架分类及关键特性
前端·算法·django
2301_799084674 小时前
Codeforces Round 1032 (Div. 3)
数据结构·c++·算法