文章目录

    • 读数据对数据没有影响,为什么还需要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.pswp.cn/news/921419.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/921419.shtml
英文地址,请注明出处:http://en.pswp.cn/news/921419.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Nestjs框架: 基于装饰器与Guards的完成RBAC权限系统设计与实现

概述 在现代权限管理系统中&#xff0c;RBAC&#xff08;基于角色的访问控制&#xff09;是广泛采用的一种模型RBAC 核心思想是通过角色来管理用户权限通过角色绑定用户、资源和权限&#xff0c;实现细粒度的访问控制为了实现这一目标&#xff0c;我们需要在数据库中设计合理的…

机器学习如何精准预测高值

一、概念理解“机器学习对于高值的预测保守”&#xff0c;这是建模里很常见的现象&#xff0c;尤其在生态、气候、遥感这类数据分布高度偏斜的场景。通常可以从以下几个角度理解&#xff1a;1. 数据分布与样本稀缺在训练集里&#xff0c;高值样本往往非常少&#xff0c;远低于中…

蜂窝物联网模组:智能门禁产品上的关键部件

随着物联网技术的快速发展&#xff0c;蜂窝物联网模组正逐步成为智能门禁系统的关键通信组件。蜂窝模组凭借其广覆盖、高可靠性和低功耗特性&#xff0c;正从传统门禁系统的补充角色转变为智能门禁的核心通信组件&#xff0c;尤其在智慧社区、商业楼宇和政府机构等场景中展现出…

[光学原理与应用-417]:非线性光学 - 线性光学(不引发频率的变化)与非线性光学(引发频率变化)的异同

一、定义与物理机制&#xff1a;线性响应 vs 非线性响应线性光学定义&#xff1a;光与物质相互作用时&#xff0c;介质的极化强度与入射光电场强度呈线性关系&#xff08;Pϵ0​χ(1)E&#xff09;&#xff0c;输出光强与输入光强成正比&#xff08;Iout​∝Iin​&#xff09;-…

深入探讨AI在三大核心测试场景中的应用

随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;软件测试领域正经历深刻变革。传统手动测试和基于规则的自动化测试已难以应对日益复杂的系统架构与海量用户行为。AI测试通过引入机器学习、自然语言处理、计算机视觉等技术&#xff0c;显著提升了测试效率、…

[linux仓库]性能加速的隐形引擎:深度解析Linux文件IO中的缓冲区奥秘

&#x1f31f; 各位看官好&#xff0c;我是egoist2023&#xff01; &#x1f30d; Linux Linux is not Unix &#xff01; &#x1f680; 今天来学习C语言缓冲区和内核缓存区的区别以及缓存类型。 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&#xff0c…

一、计算机的数据存储

计算机的世界只有0和1。 1.1 进制 十进制整数->二进制整数&#xff1a;除2倒取余二进制->十进制&#xff1a;权值相加法 结论&#xff1a;1位8进制值 3位二进制值&#xff0c;1位十六进制值 4位二进制值 public class JinZhiDemo {public static void main(String[]…

SpringBoot集成XXL-JOB保姆教程

第一步&#xff1a; 下载xxl-job源码到本地&#xff0c;地址如下&#xff1a; xxl-job: 一个分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c;开箱即用。 第二步&#xff1a; 创建…

Debezium日常分享系列之:Debezium 3.2.2.Final发布

Debezium日常分享系列之&#xff1a;Debezium 3.2.2.Final发布Debezium CoreConnector启动时出现难以理解的错误临时阻塞快照失败可能导致数据丢失的问题修复Debezium for OracleDebezium CoreConnector 启动时出现难以理解的错误 我们解决了一个问题&#xff0c;即连接器会因…

Zoom AI 技术架构研究:联合式方法与多模态集成

一、研究背景与概述 在当今数字化转型加速的背景下,人工智能技术正深刻改变企业协作与沟通方式。作为全球领先的视频会议平台,Zoom 已从单纯的通信工具转型为全面的生产力平台,而其 AI 技术架构是这一转变的核心驱动力。本报告将深入分析 Zoom 的 AI 技术架构,特别是其创新…

排序-快速排序 O(n log n)

快排&#xff1a;1、设定一个中间值 q[ lr >>1 ] , 让左右区间来比较2、左边通过 i 依次比较&#xff0c;如果比这个中间值小&#xff0c;就继续 , 直到不符合3、右边通过 j-- 依次比较&#xff0c;如果比这个中间值大&#xff0c;就继续 &#xff0c;直到不符合4、两边…

【Proteus仿真】定时器控制系列仿真——LED小灯闪烁/流水灯/LED灯带控制/LED小灯实现二进制

目录 0案例视频效果展示 0.1例子1&#xff1a;基于AT89C51单片机的定时器控制小灯闪烁 0.2例子2&#xff1a;基于AT89C51单片机的定时器T0流水灯 0.3例子3&#xff1a;基于AT89C51单片机的定时器控制LED灯带 0.4例子4&#xff1a;基于AT89C51单片机的定时器控制LED闪烁 0…

进阶向:密码生成与管理工具

密码生成与管理工具&#xff1a;从零开始的完全指南在现代数字生活中&#xff0c;密码是保护个人信息和账户安全的第一道防线。随着网络服务的普及&#xff0c;每个人平均需要管理数十个不同账户的密码。一个强大且独特的密码通常应包含12个以上字符&#xff0c;混合大小写字母…

解决 Gitee 中 git push 因邮箱隐私设置导致的失败问题

解决 Gitee 中 git push 因邮箱隐私设置导致的失败问题 在使用 Git 向 Gitee 远程仓库推送代码时&#xff0c;可能会遇到因邮箱隐私设置引发的 git push 失败情况。最近我就碰到了&#xff0c;现在把问题现象、原因和解决方法分享出来。 一、错误现象 执行 git push -u origin …

Flutter的三棵树

“三棵树”是 Flutter 渲染和构建UI的核心机制&#xff0c;理解它们对于掌握 Flutter 至关重要。这三棵树分别是&#xff1a; Widget 树 Element 树 RenderObject 树 它们协同工作&#xff0c;以实现 Flutter 的高性能渲染和高效的响应式编程模型。 Flutter 是声明式的UI&…

同一台nginx中配置多个前端项目的三种方式

目录 第一种方式:配置多个二级域名 第二种方式:配置端口转发(不推荐) 第三种方式:同一个server中基于location配置(重点讲解) 第一种方式:配置多个二级域名 一个域名下面申请多个二级域名,每个二级域名配置一个vue前端项目,这个很好配置,在这里不再详细说明。 …

第二家公司虽然用PowerBI ,可能更适合用以前的QuickBI

第二家公司虽然用PowerBI &#xff0c;可能更适合用以前的QuickBI现在回想一下&#xff0c;第二家公司数据源是MySQL &#xff0c;常规报表是用excel报表&#xff0c;另外还做了一张能发布到web的看板供运营使用。基于基本情况&#xff0c;quickbi 的早期版本是合适的&#xff…

STM32 USBx Device HID standalone 移植示例 LAT1466

关键字&#xff1a;USBx&#xff0c; Device, HID&#xff0c;standalone 1.设计目的 目前 USBx Device standalone 的官方示例较少&#xff0c;不过使用 STM32CubeMX 可以快速地生成 USBx Device 相关类的示例工程&#xff0c;会很方便大家的开发。这里以 NUCLEO-H563 为例&…

python创建并写入excel文件

大家好&#xff0c;这里是七七&#xff0c;今天来跟大家分享一个python创建并写入一个excel文件的小例子&#xff0c;话不多说&#xff0c;开始介绍。首先我们来看一下这一小段代码。import openpyxl# 创建一个新的 Excel 工作簿workbook openpyxl.Workbook()# 获取当前活动的…

react native 出现 FATAL EXCEPTION: OkHttp Dispatcher

react native 出现 FATAL EXCEPTION: OkHttp Dispatcher 报错信息FATAL EXCEPTION: OkHttp DispatcherProcess: , PID: 8868java.lang.NoSuchMethodError: No virtual method toString(Z)Ljava/lang/String; in class Lokhttp3/Cookie; or its super classes (declaration of o…