c++ 读写锁的理解

1.概要

读写锁的理解
读的时候,只要是读的线程都不受限制,但不能写。
写的时候,线程独占,任何写和读的线程都不可以。

最初我以为,只有限制写就可以了,读完全不受现在,但是有可能读到不完整的数据,比如写一半的数据等等待,所以这就是对读的那部分控制,共享锁的价值。

2.std::shared_mutex 详细说明

std::shared_mutex是C++17中引入的一个同步原语,用于在多线程环境下提升对共享资源的访问效率。与传统的互斥锁(如std::mutex)不同,std::shared_mutex允许多个线程以只读模式共享对资源的访问,但写入操作必须独占资源,以防止同时有其他线程对共享资源进行读取或写入。

关键特性

  1. 两种访问级别
    • 共享访问(读锁):多个线程可以同时拥有读锁,允许它们并行读取数据。
    • 独占访问(写锁):只有一个线程可以拥有写锁,以此来修改数据。当一个线程拥有写锁时,其他线程无法获得读锁或写锁。
  2. 适用场景:这种机制非常适合于多读少写的场景,因为它能够最大化读操作的并发性,同时确保写操作的安全性。

使用方法

为了安全地使用std::shared_mutex,你需要理解其两种锁模型:

  1. 独占锁(写锁):当你需要对共享数据进行写入操作时,应该使用独占锁。独占锁确保了在执行写入操作期间,没有其它线程对数据进行读或写操作。
  2. 共享锁(读锁):当多个线程需要读取共享数据但不进行修改时,可以使用共享锁。多个线程可以同时拥有读锁,但如果有线程正在使用独占锁(写锁),则其他线程无法获得读锁或写锁。

示例

下面是一个简单的示例,展示了如何使用std::shared_mutex来同步对共享资源的访问:

#include <iostream>  
#include <thread>  
#include <vector>  
#include <shared_mutex>  std::shared_mutex mtx;  
int shared_data = 0;  void reader(int id) {  std::shared_lock<std::shared_mutex> lock(mtx);  // 读取共享数据  std::cout << "Reader " << id << " reads: " << shared_data << std::endl;  
}  void writer(int value) {  std::unique_lock<std::shared_mutex> lock(mtx);  // 修改共享数据  shared_data = value;  std::cout << "Writer writes: " << shared_data << std::endl;  
}  int main() {  std::vector<std::thread> threads;  // 创建多个读线程  for (int i = 0; i < 5; ++i) {  threads.emplace_back(reader, i);  }  // 创建一个写线程,稍后进行写入操作  threads.emplace_back(writer, 42);  // 等待所有线程完成  for (auto& t : threads) {  t.join();  }  return 0;  
}

请注意,上述示例中的写线程在创建后并不会立即执行,因为线程的调度顺序是由操作系统决定的。为了更精确地控制读写操作的顺序,你可能需要使用条件变量或其他同步机制。

3.共享锁和没有锁有啥区别

C++中的共享锁(如std::shared_lock配合std::shared_mutex使用)和没有锁(即不使用任何同步机制)之间存在显著的区别。这些区别主要体现在对多线程环境中共享资源访问的控制和安全性上。

没有锁

如果不使用任何锁或同步机制,多个线程可以自由访问和修改共享资源,这可能导致以下问题:

  1. 数据竞争(Data Race):当两个或更多的线程并发访问同一内存位置,且至少有一个线程是写入操作,并且线程间没有使用适当的同步来协调这些访问时,就会发生数据竞争。数据竞争会导致未定义的行为,因为写入操作的顺序是不确定的。
  2. 不一致的状态:由于线程之间没有协调它们的访问,一个线程可能看到由另一个线程部分修改的变量,导致程序处于不一致的状态。
  3. 难以调试:没有锁的多线程程序通常很难调试,因为线程的执行顺序是不确定的,并且程序的行为可能因环境而异(例如,CPU核心数量、线程调度策略等)。

共享锁

共享锁允许多个线程同时读取共享资源,但阻止任何线程在已有线程读取或写入时写入资源。使用共享锁可以解决上述问题:

  1. 防止数据竞争:通过确保在读取或写入共享资源时只有一个线程可以执行,共享锁防止了数据竞争。
  2. 保持数据一致性:由于写入操作是独占的,因此当一个线程正在写入时,其他线程无法读取或写入,从而确保了数据的一致性。
  3. 提高并发性:与互斥锁(如std::mutex)相比,共享锁允许多个线程同时读取共享资源,从而提高了并发性。这对于多读少写的场景特别有用。
  4. 简化编程:使用共享锁可以简化多线程编程,因为程序员不需要手动协调线程之间的访问,而是可以依靠锁机制来自动处理。

总结

没有锁的多线程程序可能导致数据竞争、不一致的状态和难以调试的问题。而共享锁通过确保在读取或写入共享资源时只有一个线程可以执行,解决了这些问题,并提高了并发性。因此,在需要多线程访问共享资源的场景中,使用适当的锁或同步机制是非常重要的。

4.读写锁和普通锁的对比试验

为了对比C++中的读写锁(std::shared_mutex 或其他第三方读写锁实现)和普通锁(如 std::mutex)的性能,我们可以设计一个简单的试验。在这个试验中,我们将创建多个线程,其中一些线程作为读者(只读取数据),而另一些线程作为写者(修改数据)。

以下是一个简化的示例,展示了如何使用std::mutexstd::shared_mutex来模拟这种场景,并进行性能对比。

使用 std::mutex

#include <iostream>  
#include <thread>  
#include <vector>  
#include <mutex>  
#include <chrono>  std::mutex mtx;  
int data = 0;  void reader(int id) {  for (int i = 0; i < 10000; ++i) {  std::lock_guard<std::mutex> lock(mtx);  // 假设我们只是读取数据  int value = data;  // ...(省略其他读取操作)  }  
}  void writer(int id) {  for (int i = 0; i < 1000; ++i) {  std::lock_guard<std::mutex> lock(mtx);  // 假设我们只是写入数据  data = id;  // ...(省略其他写入操作)  std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟耗时写入  }  
}  int main() {  auto start = std::chrono::high_resolution_clock::now();  std::vector<std::thread> threads;  for (int i = 0; i < 5; ++i) {  threads.emplace_back(reader, i);  }  for (int i = 0; i < 2; ++i) {  threads.emplace_back(writer, i);  }  for (auto& t : threads) {  t.join();  }  auto end = std::chrono::high_resolution_clock::now();  std::chrono::duration<double> diff = end - start;  std::cout << "Time taken with std::mutex: " << diff.count() << " s\n";  return 0;  
}

使用 std::shared_mutex

 
#include <iostream>  
#include <thread>  
#include <vector>  
#include <shared_mutex>  
#include <chrono>  std::shared_mutex smtx;  
int data = 0;  void reader(int id) {  for (int i = 0; i < 10000; ++i) {  std::shared_lock<std::shared_mutex> lock(smtx);  // 假设我们只是读取数据  int value = data;  // ...(省略其他读取操作)  }  
}  void writer(int id) {  for (int i = 0; i < 1000; ++i) {  std::unique_lock<std::shared_mutex> lock(smtx, std::defer_lock);  lock.lock(); // 使用unique_lock来锁定,因为shared_lock不允许升级锁  // 假设我们只是写入数据  data = id;  // ...(省略其他写入操作)  std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模拟耗时写入  }  
}  // main函数与上述类似,只是锁的类型改变了

注意:在上面的std::shared_mutex示例中,我们使用std::unique_lock来锁定写操作,因为std::shared_lock不允许锁升级(即,从共享锁升级到独占锁)。但是,为了保持示例的简单性,我们仍然使用std::shared_mutex来展示读写锁的基本概念。

性能对比

要获得准确的性能对比,您应该多次运行这两个程序,并可能使用更复杂的基准测试工具来测量CPU时间、线程争用等。但是,从概念上讲,当读者数量远多于写者时,使用std::shared_mutex(或类似的读写锁)应该比使用std::mutex具有更好的性能,因为多个读者可以同时访问数据,而不会被彼此阻塞。

5.defer_lock 啥意思

在C++中,std::defer_lock是一个标签类型,它用于与互斥体(std::mutexstd::timed_mutexstd::recursive_mutexstd::recursive_timed_mutexstd::shared_mutex等)的std::unique_lockstd::shared_lock结合使用,以指示在构造时不立即锁定互斥体。

具体来说,当你使用std::unique_lockstd::shared_lock时,默认情况下,互斥体会在std::unique_lockstd::shared_lock的构造函数中被锁定。但是,如果你使用std::defer_lock标签,则互斥体不会在构造函数中立即锁定,你可以稍后通过调用lock()try_lock()unlock()等方法来控制锁定和解锁。

这在某些情况下可能是有用的,例如:

  1. 条件变量:当你使用条件变量(如std::condition_variable)时,你可能希望在等待条件变量之前不锁定互斥体,因为条件变量的wait()方法会自动解锁互斥体并在条件满足时重新锁定它。在这种情况下,你可以使用std::defer_lock来构造一个未锁定的std::unique_lock,然后调用wait()
  2. 复杂的同步逻辑:在更复杂的同步场景中,你可能希望根据某些条件来决定是否立即锁定互斥体。使用std::defer_lock可以让你延迟这个决策。

下面是一个使用std::defer_lock的示例:

#include <iostream>  
#include <mutex>  
#include <thread>  
#include <chrono>  std::mutex mtx;  void print_block(int n, char c) {  std::unique_lock<std::mutex> lck(mtx, std::defer_lock);  // 在这里,mtx还没有被锁定  // 模拟一些工作  std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 现在我们决定锁定互斥体  lck.lock();  for (int i = 0; i < n; ++i) {  std::cout << c;  }  std::cout << '\n';  // 解锁(但在这个例子中,当lck离开作用域时会自动解锁)  lck.unlock();  // 或者简单地让lck离开作用域,它会在析构时自动解锁  
}  int main() {  std::thread th1(print_block, 50, '*');  std::thread th2(print_block, 50, '$');  th1.join();  th2.join();  return 0;  
}

注意,在这个示例中,尽管我们使用了std::defer_lock,但实际上我们还是在lck.lock()调用时立即锁定了互斥体。但是,这个例子展示了如何使用std::defer_lock来延迟锁定决策。在更复杂的场景中,你可能会根据某些条件来决定是否调用lock()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/10134.shtml

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

相关文章

【初始类和对象】(实例讲解!超级详细!)

【初始类和对象】 前言1. 面向对象的初步认知1.1什么是面向对象1.2 面向对象与面向过程 2. 类的定义和使用2.1 简单认识类2.2 类的定义格式 3. 知识的代码举例讲解3.1 创建类、实例化类3.2 构造方法初始化类、this 3. 总结 前言 由于类和对象是我们在学习过程中需要接受的概念…

AI赋能未来教育:中国教学科研新蓝图

设“人啊 前言 回顾过去&#xff0c;传统的教育模式以知识灌输和应试为主&#xff0c;虽培养出大量人才&#xff0c;但也存在着学生创新能力不足、实践经验缺乏等问题。随着时代的进步和科技的发展&#xff0c;传统教育模式已难以满足当今社会对人才的需求。然而&#xff0c;当…

LoadIncrementalHFiles 流程和原理

目录 1. HBase Bulk Load 简介 2. 流程 3. 原理 4. 使用注意事项 5.补充说明之"什么是移动文件" 1. HBase Bulk Load 简介 LoadIncrementalHFiles是用于HBase的Bulk Load工具&#xff0c;允许用户高效地将大量数据直接加载到HBase表中&#xff0c;而不是使用传…

中国现代十大杰出人物颜廷利:好的司机不如好的同机

找好‘同机’者, 要比找好‘司机’者, 原因就是, ‘司机’虽好, 但不是‘同路人’, 再多努力的攀附都是徒劳, 至于‘同机’者, 即便是对方在自己的眼里心中都一无是处, 只不过, 他/她才是您旅途之中, 真真正正、风雨同舟的人…(升命学说) 21世纪东方哲学家思想家、科学家、当代…

孩子学编程和不学编程的差距?

随着信息技术的飞速发展&#xff0c;编程已经成为一项非常重要的技能&#xff0c;不仅仅是在计算机领域&#xff0c;而且在各个行业都有着广泛的应用。因此&#xff0c;让孩子学习编程已经成为很多家长的选择。那么&#xff0c;孩子学习编程和不学习编程之间有哪些差距呢&#…

TODESK远控快捷键在哪里

在当今高度数字化的世界中&#xff0c;远程工作和协作已经成为日常生活和业务运营的重要组成部分。Todesk作为一款出色的远程协作软件&#xff0c;为用户提供了诸多功能&#xff0c;以确保流畅、高效的远程连接体验。其中&#xff0c;快捷键功能极大地提升了用户的操作便捷性。…

高速、简单、安全的以太彩光,锐捷网络发布极简以太全光 3.X 方案

从 2021 年 3 月正式推出到现在&#xff0c;锐捷网络极简以太全光方案已经走进第四个年头。IT 仍在不断向前发展&#xff0c;数字化进程深入&#xff0c;数字化业务增多&#xff0c;更广泛的终端设备接入企业级园区网络&#xff0c;对园区网络提出了更高的要求&#xff0c;例如…

GDB断点执行的次数

需求背景&#xff1a;条件断点可能执行多次&#xff0c;但是可能在最后一次执行引发了后续的问题&#xff0c;但是断点位置并非问题现场&#xff0c;如何使得断点在最后一次停下来&#xff1f; 方法&#xff1a; 1.首先设置条件断点 (gdb) b (gdb) cond breakpoint_number…

Linux NFS共享目录配置漏洞

Linux NFS共享目录配置漏洞 一、实验目的二、实验原理三、复现准备四、漏洞复现4.1、复现前提4.2、正式复现 一、实验目的 利用 NFS共享目录配置漏洞读取目标主机的 /etc/passwd 文件内容NFS 服务配置漏洞&#xff0c;赋予了根目录远程可写权限&#xff0c;导致 /root/.ssh/au…

关系型数据库VS非关系型数据库

数据库是存储和组织数据的系统&#xff0c;主要分为两大类&#xff1a; 关系型数据库&#xff08;Relational Database Management Systems, RDBMS&#xff09; 非关系型数据库&#xff08;NoSQL Databases&#xff09; 下面分别介绍这些类型及其区别&#xff1a; 关系型数…

im8mm 网络卡死 Rx packets:1037578 errors:66 dropped:0 overruns:66 frame:0

1&#xff1a;网络接收数据包异常 2&#xff1a;问题复现 问题在进行网络数据包同吞吐量测试的时候出现的。同时发现&#xff0c;在使用iperf2测试时&#xff0c;是不会出现网络中断卡死的情况&#xff0c;使用 iperf3时才会出现此问题 指令(下面的指令运行在PC2上面&#xff…

AGV混合型电机驱动器|伺服控制器CNS-MI50H系列对电机的要求

混合型电机驱动器 CNS-MI50H系列涵盖CNS-MI50HB-A、CNS-MI50HBN-A、CNS-MI50HDN-A、CNS-MI50HSN-A型号&#xff0c;专为 AGV 舵轮控制需求设计&#xff0c;集成舵轮转向角度控制和驱动电机闭环控制。支持增量式编码器&#xff0c;霍尔传感器&#xff0c; 角度电位计&#xff0c…

自动化测试基础 --- Jmeter

前置环境安装 首先我们需要知道如何下载Jmeter 这里贴上下载网站Apache JMeter - Download Apache JMeter 我们直接解压,然后在bin目录下找到jemter.bat即可启动使用 成功打开之后就是这个界面 每次打开可以用这种方式切换成简体中文 或者直接修改properties文件修改对应的语言…

目标检测算法YOLOv8简介

YOLOv8论文尚未发布&#xff0c;YOLOv8由Ultralytics公司推出并维护&#xff0c;源码见&#xff1a;https://github.com/ultralytics/ultralytics &#xff0c;于2024年1月发布v8.1.0版本&#xff0c;最新发布版本为v8.2.0&#xff0c;License为AGPL-3.0。 以下内容主要来自&am…

FFmpeg 中 -f 命令参数详解

FFmpeg FFmpeg是一个开源的、功能强大的多媒体框架,它能够处理几乎所有格式的音频和视频文件。FFmpeg由Fabrice Bellard创立,并由Michael Niedermayer等人继续开发。它包括了libavcodec(用于编解码)、libavformat(用于格式转换)、libavfilter(用于音视频过滤)、libavd…

微信授权登录01-PC端

目录 ## 前言 1.准备工作 1.1 网站域名 1.2 微信开放平台 2.授权授权登录开发 2.1 前端开发 2.1.1 发起授权登录跳转至扫码页面 2.1.2 扫码成功后回调处理 2.2 后端开发 2.2.1 根据code查询用户信息 2.2.2 自动注册登录 ## 后记 ## 前言 最近整了个AI助手网站&am…

React 学习-5

React 条件渲染: 与js中的写法一致 注意&#xff1a;在 JavaScript 中&#xff0c;true && expression 总是返回 expression&#xff0c;而 false && expression 总是返回 false。 因此&#xff0c;如果条件是 true&#xff0c;&& 右侧的元素就会被渲…

BL120协议Modbus RTU和Modbus TCP互转

Modbus网关BL120是一款专注于Modbus协议之间相互转换的通信设备。Modbus网关BL120支持多种下行采集协议&#xff0c;包括Modbus RTU和Modbus TCP&#xff0c;同时在上行转发协议方面同样支持Modbus RTU和Modbus TCP。Modbus网关为Modbus RTU和Modbus TCP协议的相互转换提供了稳…

回炉重造java----单列集合(List,Set)

体系结构: 集合主要分为两种&#xff0c;单列集合collection和双列集合Map&#xff0c;区别在于单列集合一次插入一条数据&#xff0c;而双列的一次插入类似于key-value的形式 单列集合collection 注:红色的表示是接口&#xff0c;蓝色的是实现类 ①操作功能: 增加: add()&am…

SRS流媒体服务器在Linux下的安装

目录 一、安装 1、切换到管理员权限 2、先安装基础依赖环境 3、下载SRS源文件