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

读写锁 shared_mutex 共享互斥量介绍

文章目录

    • 读数据对数据没有影响,为什么还需要shared_mutex
      • 1. 保证读取数据的“一致性”和“时效性”
      • 2. 协调“读”与“写”的竞争关系
      • 总结
    • 好的,我们来详细介绍 C++17 中的 `std::shared_mutex`(共享互斥量,俗称读写锁)的使用。
      • 核心概念
      • 包含头文件
      • 基本使用步骤
        • 1. 定义共享数据和共享互斥量
        • 2. 读操作 - 使用 `std::shared_lock`
        • 3. 写操作 - 使用 `std::unique_lock` 或 `std::lock_guard`
      • 完整示例代码
      • 可能输出及分析:
      • 重要注意事项

读数据对数据没有影响,为什么还需要shared_mutex

这是一个非常经典且重要的问题。您的直觉是正确的——单纯的读操作本身确实不会改变数据。但关键在于,我们使用 std::shared_mutex(共享锁/读写锁)的目的,不仅仅是为了防止读操作“搞破坏”,更是为了保护读操作自身能获得一个正确、可靠的结果

核心原因在于:并发编程的世界里,您不能只考虑一个线程的行为,必须考虑多个线程同时操作同一份数据时可能发生的交互和冲突。

让我们用一个比喻来理解:

想象一下,您(读线程)正在阅读一本非常重要的参考书(数据)。

  • 没有锁的场景:当您正在阅读第100页时,图书管理员(写线程)突然过来把第100页撕掉,换成了新的一页。您读到的内容就变成了半句旧话和半句新话的混合体,这显然是错误且无意义的。这就是脏读(Dirty Read)
  • 使用 std::mutex 的场景:为了保护书的内容,图书馆规定一次只允许一个人进入(独占锁)。无论您是去阅读(读)还是去修改(写),都要排队。这非常安全,但效率极低,因为明明可以允许多个人同时阅读。
  • 使用 std::shared_mutex 的场景:图书馆现在有了新规则:允许多个人同时阅读(共享锁),但只要有人需要修改书籍(写线程申请独占锁),就会阻止新的读者进入,并等待所有现有的读者离开后,才进行修改。修改完成后,再允许新的读者进入。这既保证了效率(多人同时读),又保证了安全(读的时候书不会变,写的时候是独占的)。

从技术角度,主要有以下两个问题需要解决:


1. 保证读取数据的“一致性”和“时效性”

即使读操作不修改数据,它也需要读到某个特定时间点的、完整一致的数据。

  • 问题一:脏读 (Dirty Read)

    • 场景:写线程B开始修改数据(例如,分两步更新一个结构体),刚更新到一半。
    • 此时:读线程A来读取这个数据。它读到的是一半新、一半旧的中间状态,这数据是无效的、从未正式存在过的“脏”数据。
    • 共享锁的作用:读线程A持有共享锁,会阻止写线程B获取独占锁。因此,写操作根本无法开始,读操作读到的绝对是写操作开始前的一致状态。
  • 问题二:读到一个“正在变化”的值

    • 场景:数据可能不是一个简单的 int,而是一个需要多条指令才能更新的复杂结构(例如,一个链表头指针)。写线程的更新操作可能不是原子的。
    • 此时:读线程可能在写线程更新到一半时介入,读到错误的指针,导致程序崩溃或得到错误结果。
    • 共享锁的作用:同样,共享锁阻止了写线程的进入,保证了读线程看到的对象状态始终是完整的、稳定的。

2. 协调“读”与“写”的竞争关系

这是您问题描述中的关键点,也是 std::shared_mutex 设计的精髓。

  • 写操作需要独占性:写操作必须保证在它修改数据的那一刻,没有任何其他线程(无论是读还是写)正在访问它要修改的数据。否则就无法安全地更新。
  • 共享锁是给写操作看的“信号”:读线程加共享锁,本质上是在向写线程宣告:“我正在读这份数据,你(写线程)现在不能写!
    • 只要有一个读线程持有共享锁,写线程的独占锁请求就会被阻塞。
    • 多个读线程的共享锁不会互相阻塞,但它们“合力”阻塞了写线程。
    • 写线程持有独占锁时,会阻塞所有新的读线程(共享锁)和其他写线程。

总结

所以,回到您的问题:读数据对数据没有影响,为什么还需要 shared_mutex

  1. 为了保护读操作自身:确保它不会读到写操作产生的中间状态或脏数据,从而保证读取结果的正确性和一致性
  2. 为了协调读与写的竞争:共享锁是一个高效的“信号机制”,它允许多个读操作并行(提升性能),同时强制读操作和写操作之间互斥(保证安全)。它实现了您所说的“读读共享、读写互斥、写写互斥”的完美规则。

如果没有共享锁,在C++应用程序层面,单纯的读操作在多线程环境下就是“裸奔”,无法保证自己能读到有意义的数据。std::shared_mutex 就是为读操作穿上的一件防护服,它不阻止其他读操作也穿上同样的防护服(共享),但能有效阻止写操作(独占)进来搞破坏。

好的,我们来详细介绍 C++17 中的 std::shared_mutex(共享互斥量,俗称读写锁)的使用。

核心概念

std::shared_mutex 的核心是区分两种访问模式,对应两种锁:

  1. 共享模式 (Shared Mode) - 用于“读”

    • 多个线程可以同时获得共享锁。
    • 当一个或多个线程持有共享锁时,任何请求独占锁的线程都会被阻塞。
    • 使用 std::shared_lock 来管理共享锁。
  2. 独占模式 (Exclusive Mode) - 用于“写”

    • 只有一个线程可以获得独占锁。
    • 当一线程持有独占锁时,任何其他请求共享锁独占锁的线程都会被阻塞。
    • 使用 std::unique_lockstd::lock_guard 来管理独占锁。

包含头文件

#include <shared_mutex> // 主要头文件
#include <mutex>        // 用于 std::unique_lock, std::lock_guard
#include <map>
#include <string>
#include <thread>

基本使用步骤

1. 定义共享数据和共享互斥量

将你需要保护的数据和对应的 std::shared_mutex 放在一起,通常作为类的私有成员。

class ThreadSafeDNSCache {
private:std::map<std::string, std::string> dns_map_;mutable std::shared_mutex mutex_; // ‘mutable’ 允许在 const 成员函数中加共享锁
};
2. 读操作 - 使用 std::shared_lock

对于不会修改数据的操作(如 find, get),使用 std::shared_lock。它会在构造时自动上共享锁,析构时自动解锁。

std::string ThreadSafeDNSCache::find_ip(const std::string& domain) const {std::shared_lock<std::shared_mutex> lock(mutex_); // 获取共享锁// 注意:这里是 const 成员函数,因为find操作不应修改数据auto it = dns_map_.find(domain);if (it != dns_map_.end()) {return it->second; // 返回时,lock 析构,自动释放共享锁}return "Not Found";
}
3. 写操作 - 使用 std::unique_lockstd::lock_guard

对于会修改数据的操作(如 insert, update, erase),使用 std::unique_lockstd::lock_guard。它们会在构造时自动上独占锁,析构时自动解锁。

std::unique_lockstd::lock_guard 更灵活(例如可以手动解锁),但开销稍大。对于简单作用域,std::lock_guard 就足够了。

void ThreadSafeDNSCache::update_or_add(const std::string& domain, const std::string& ip) {std::unique_lock<std::shared_mutex> lock(mutex_); // 获取独占锁dns_map_[domain] = ip;
} // lock 析构,自动释放独占锁void ThreadSafeDNSCache::clear_all() {std::lock_guard<std::shared_mutex> lock(mutex_); // 同样获取独占锁dns_map_.clear();
}

完整示例代码

#include <iostream>
#include <map>
#include <string>
#include <shared_mutex>
#include <thread>
#include <chrono>class ThreadSafeDNSCache {
public:std::string find_ip(const std::string& domain) const {// 1. 尝试获取共享锁(读锁)std::shared_lock<std::shared_mutex> lock(mutex_);std::cout << "Reading domain: " << domain << std::endl;// 模拟一个耗时较长的读操作std::this_thread::sleep_for(std::chrono::milliseconds(100));auto it = dns_map_.find(domain);if (it != dns_map_.end()) {std::cout << "Found IP: " << it->second << " for domain: " << domain << std::endl;return it->second;}std::cout << "Domain not found: " << domain << std::endl;return "Not Found";}void update_or_add(const std::string& domain, const std::string& ip) {// 2. 尝试获取独占锁(写锁)std::unique_lock<std::shared_mutex> lock(mutex_);std::cout << "Updating/Adding domain: " << domain << " -> " << ip << std::endl;// 模拟一个耗时较长的写操作std::this_thread::sleep_for(std::chrono::milliseconds(500));dns_map_[domain] = ip;std::cout << "Finished updating: " << domain << std::endl;}private:mutable std::shared_mutex mutex_;std::map<std::string, std::string> dns_map_;
};int main() {ThreadSafeDNSCache cache;// 启动多个读线程和一个写线程来演示效果std::thread reader1([&cache]() { cache.find_ip("github.com"); });std::thread reader2([&cache]() { cache.find_ip("google.com"); });std::thread writer([&cache]() {std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 稍等一下,让读线程先启动cache.update_or_add("github.com", "140.82.112.4");});std::thread reader3([&cache]() { cache.find_ip("github.com"); }); // 这个读操作会在写之后开始reader1.join();reader2.join();writer.join();reader3.join();return 0;
}

可能输出及分析:

输出可能会是这样的(顺序可能略有不同):

Reading domain: github.com    // 读者1 立即获取共享锁,开始读
Reading domain: google.com    // 读者2 也立即获取共享锁,和读者1同时读
// ... 读者1和2 几乎同时完成他们的读操作 ...
Updating/Adding domain: github.com -> 140.82.112.4 // 写者 等待读者1和2释放共享锁后,获取独占锁,开始写
Finished updating: github.com // 写者 完成写操作,释放独占锁
Reading domain: github.com    // 读者3 在写者释放锁后,获取共享锁,开始读(会读到新值)
Found IP: 140.82.112.4 for domain: github.com

这个输出完美展示了:

  • 读读并行reader1reader2 同时执行。
  • 读写互斥writer 必须等待所有现有的读者 (reader1, reader2) 结束后才能开始。
  • 写写互斥:(本例未展示第二个写者)如果有第二个写者,它也会被阻塞。
  • 写后读reader3writer 阻塞,直到写操作完成,从而保证了它读到的是最新值。

重要注意事项

  1. mutable 关键字:如果你的“读”操作是 const 成员函数(它应该是),但你又需要在其中修改 mutex_(加锁解锁属于“物理常量性”修改,而非“逻辑常量性”),必须用 mutable 修饰 mutex_
  2. 递归使用std::shared_mutex 是不可递归的。同一个线程试图在已获得共享锁的情况下再获取独占锁(或反之)会导致未定义行为(通常是死锁)
  3. 升级锁:不能直接将已持有的共享锁“升级”为独占锁。你必须先释放共享锁,然后再尝试获取独占锁。这个过程不是原子的,中间可能被其他写线程插队。
  4. 性能:虽然读写锁在“读多写少”的场景下性能优异,但其内部实现比普通互斥量更复杂,开销也稍大。如果临界区非常小,或者写操作很频繁,可能普通的 std::mutex 性能更好。永远基于性能测试来做选择
http://www.xdnf.cn/news/1482877.html

相关文章:

  • Dart HashMap:不保证顺序的 Map 实现
  • (二).net面试(static)
  • MySQL--索引和事务
  • simd学习
  • esbuild入门
  • Cursor安装使用 与 Cursor网页端登录成功,客户端怎么也登陆不上
  • 解析噬菌体实验核心:从材料选择到功能验证的标准化操作框架
  • 数据结构——队列(Java)
  • 基于STM32单片机的酒驾检测设计
  • OpenAvatarChat项目在Windows本地运行指南
  • 【基础-单选】关于自定义组件的生命周期下列说法错误的是
  • 四款主流深度相机在Python/C#开发中的典型案例及技术实现方案
  • vant组件
  • 昇腾310i Pro固件说明
  • Vue3中SCSS的使用指南
  • 数据结构与算法1 第一章 绪论
  • AI工具深度测评与选型指南 - AI工具测评框架及方法论
  • Gitea:轻量级的自托管Git服务
  • 【左程云算法06】链表入门练习合集
  • GDAL 读取影像元数据
  • SQL-窗口函数
  • 单词分析与助记之数据建表(以production为例)
  • 鸡兔同笼问题求解
  • 手撕C++ list容器:从节点到完整双向链表实现
  • Ubuntu 22.04.1上安装MySQL 8.0及设置root密码
  • 贪心算法应用:柔性制造系统(FMS)刀具分配问题详解
  • 深度拆解OpenHarmony NFC服务:从开关到卡模拟掌握近场通信技术
  • 雷卯针对米尔MYC-YF13X开发板防雷防静电方案
  • vspere 服务的部署介绍
  • panther X2 armbian24 安装宝塔(bt)面板注意事项