当前位置: 首页 > news >正文

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 是一个非常值得使用的工具,它能显著地提升你程序的并发性能。

相关文章:

  • 基于YOLOv10算法的交通信号灯检测与识别
  • Arduino入门教程:11、直流步进驱动
  • 选择标签词汇功能(单选多选),在文本框展示
  • DeepSeek 助力 Vue3 开发:打造丝滑的日历(Calendar),日历_项目里程碑示例(CalendarView01_22)
  • LeetCode 1358.包含所有三种字符的子字符串数目
  • 暑期前端训练day1
  • 前端适配方案之 flexible.js 到 postcss-px-to-viewport-8-plugin插件演进
  • Windows 10开始菜单优化方案,如何实现Win7风格开始菜单的还原
  • 【设计模式】用观察者模式对比事件订阅(相机举例)
  • 【K8S】详解NodePort 和 ClusterIP
  • 【K8S】详解Labels​​ 和 ​​Annotations
  • Android 应用多语言与系统语言偏好设置指南
  • 容器运行时保护:用Falco构建云原生安全防线
  • 简单理解HTTP/HTTPS协议
  • 基于 Apache POI 实现的 Word 操作工具类
  • AI公文写作,推荐AI材料星!
  • vue3 动态绑定 ref 并获取其 dom
  • Python 自动化运维与DevOps实践
  • Docker如何实现容器之间的通信
  • 李沐动手深度学习(pycharm中运行笔记)——12.权重衰退
  • wordpress 文章列表/seo辅助工具
  • 网站建设意向表/百度下载并安装到桌面
  • c 做的网站/免费二级域名查询网站
  • 晋江网站建设哪家好/今日全国最新疫情通报
  • 建b2c网站需要办的手续/官网优化哪家专业
  • 做设计的公司的网站/购买模板建站