Linux读写锁的容易犯的问题

Linux读写锁的容易犯的问题

读写锁是互斥锁之外的另一种用于多线程之间同步的一种方式。

多线程对于一个共享变量的读操作是安全的, 而写操作是不安全的。如果在一个读很多而写很少的场景之下,那么使用互斥锁将会阻碍大量的线程安全的读操作的进行。在这种场景下,读写锁这样一种设计便诞生了。

读写锁的特性如下表所示, 总结起来就是读读不互斥读写互斥写写互斥

不互斥互斥
互斥互斥

看似这样好的一个设计在实际的使用中确存在诸多的使用误区,陈硕大神在他的<<Linux多线程服务端编程>>一书中曾给出他的建议,不要使用读写锁。 为什么如此呢? 下面一一道来。

读写锁使用的正确性

读写锁第一个容易出错的地方就是可能在持有读锁的地方修改了共享数据。对于一些比较简单的方法可能是不容易出错的,但是对于嵌套调用的场景下,也是容易犯错的。例如下面的例子,read方法持有了读锁,但是operator4会修改共享变量。由于operator4的调用深度较深,因此可能容易犯错。

//operator4会修改共享变量
void operation4();
{//...
}void operation3()
{operation4();
}void operation2()
{operation3();
}void read() {std::shared_lock<std::shared_mutex> lock(mtx);operation1();
}

读写锁性能上的开销

读写锁从设计上看是比互斥锁要复杂一些,因此其内部加锁和解锁的逻辑也要比互斥锁要复杂。

下面是glibc读写锁的数据结构,可以推测在加锁解锁过程中要更新reader和writers的数目,而互斥锁是无需这样的操作的。

struct __pthread_rwlock_arch_t
{unsigned int __readers;unsigned int __writers;unsigned int __wrphase_futex;unsigned int __writers_futex;unsigned int __pad3;unsigned int __pad4;int __cur_writer;int __shared;unsigned long int __pad1;unsigned long int __pad2;/* FLAGS must stay at this position in the structure to maintainbinary compatibility.  */unsigned int __flags;
};

下面的一个例子使用互斥锁和读写锁分别对一个临界区进行反复的加锁和解锁。因为临界区没有内容,因此开销基本都在锁的加锁和解锁上。

//g++ test1.cpp -o test1
#include <pthread.h>
#include <iostream>
#include <unistd.h>pthread_mutex_t mutex;
int i = 0;void *thread_func(void* args) {int j;for(j=0; j<10000000; j++) {pthread_mutex_lock(&mutex);// testpthread_mutex_unlock(&mutex);}pthread_exit((void *)0);
}int main(void) {pthread_t id1;pthread_t id2;pthread_t id3;pthread_t id4;pthread_mutex_init(&mutex, NULL);pthread_create(&id1, NULL, thread_func, (void *)0);pthread_create(&id2, NULL, thread_func, (void *)0);pthread_create(&id3, NULL, thread_func, (void *)0);pthread_create(&id4, NULL, thread_func, (void *)0);pthread_join(id1, NULL);pthread_join(id2, NULL);pthread_join(id3, NULL);pthread_join(id4, NULL);pthread_mutex_destroy(&mutex);
}
//g++ test2.cpp -o test2
#include <pthread.h>
#include <iostream>
#include <unistd.h>pthread_rwlock_t rwlock;
int i = 0;void *thread_func(void* args) {int j;for(j=0; j<10000000; j++) {pthread_rwlock_rdlock(&rwlock);//test2pthread_rwlock_unlock(&rwlock);}pthread_exit((void *)0);
}int main(void) {pthread_t id1;pthread_t id2;pthread_t id3;pthread_t id4;pthread_rwlock_init(&rwlock, NULL);pthread_create(&id1, NULL, thread_func, (void *)0);pthread_create(&id2, NULL, thread_func, (void *)0);pthread_create(&id3, NULL, thread_func, (void *)0);pthread_create(&id4, NULL, thread_func, (void *)0);pthread_join(id1, NULL);pthread_join(id2, NULL);pthread_join(id3, NULL);pthread_join(id4, NULL);pthread_rwlock_destroy(&rwlock);
}
[root@localhost test1]# time ./test1real    0m2.531s
user    0m5.175s
sys     0m4.200s
[root@localhost test1]# time ./test2real    0m4.490s
user    0m17.626s
sys     0m0.004s

可以看出,单纯从加锁和解锁的角度看,互斥锁的性能要好于读写锁。

当然这里测试时,临界区的内容时空的,如果临界区较大,那么读写锁的性能可能会优于互斥锁。

不过在多线程编程中,我们总是会尽可能的减少临界区的大小,因此很多时候,读写锁并没有想象中的那么高效。

读写锁容易造成死锁

前面提到过读写锁这样的设计就是在读多写少的场景下产生的,然而这样的场景下,很容易造成写操作的饥饿。因为读操作过多,写操作不能拿到锁,造成写操作的阻塞。

因此,写操作获取锁通常拥有高优先级。

这样的设定对于下面的场景,将会造成死锁。假设有线程A、B和锁,按如下时序执行:

  • 1、线程A申请读锁;
  • 2、线程B申请写锁;
  • 3、线程A再次申请读锁;

第2步中,线程B在申请写锁的时候,线程A还没有释放读锁,于是需要等待。第3步中,因此线程B正在申请写锁,于是线程A申请读锁将会被阻塞,于是陷入了死锁的状态。

下面使用c++17的shared_mutex来模拟这样的场景。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <shared_mutex>void print() {std::cout << "\n";
}
template<typename T, typename... Args>
void print(T&& first, Args&& ...args) {std::cout << first << " ";print(std::forward<Args>(args)...);
}std::shared_mutex mtx;
int step = 0;
std::mutex cond_mtx;
std::condition_variable cond;void read() {//step0: 读锁std::shared_lock<std::shared_mutex> lock(mtx);std::unique_lock<std::mutex> uniqueLock(cond_mtx);print("read lock 1");//通知step0结束++step;cond.notify_all();//等待step1: 写锁 结束cond.wait(uniqueLock, []{return step == 2;});uniqueLock.unlock();//step2: 再次读锁std::shared_lock<std::shared_mutex> lock1(mtx);print("read lock 2");
}void write() {//等待step0: 读锁 结束std::unique_lock<std::mutex> uniqueLock(cond_mtx);cond.wait(uniqueLock, []{return step == 1;});uniqueLock.unlock();//step1: 写锁std::lock_guard<std::shared_mutex> lock(mtx);uniqueLock.lock();print("write lock");//通知step1结束++step;cond.notify_all();uniqueLock.unlock();
}int main() {std::thread t_read{read};std::thread t_write{write};t_read.join();t_write.join();return 0;
}

可以使用下面的在线版本进行测试。

have a try

在线版本的输出是下面这样的,程序由于死锁执行超时被杀掉了。

Killed - processing time exceeded
Program terminated with signal: SIGKILL

死锁的原因就是线程1与线程2相互等待导致。

shared_mutex

对于glibc的读写锁,其提供了读优先写优先的属性。

使用pthread_rwlockattr_setkind_np方法即可设置读写锁的属性。其拥有下面的属性:

  • PTHREAD_RWLOCK_PREFER_READER_NP, //读者优先(即同时请求读锁和写锁时,请求读锁的线程优先获得锁)
  • PTHREAD_RWLOCK_PREFER_WRITER_NP, //不要被名字所迷惑,也是读者优先
  • PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, //写者优先(即同时请求读锁和写锁时,请求写锁的线程优先获得锁)
  • PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP // 默认,读者优先

glibc的读写锁模式是读优先的。下面分别使用读优先写优先来进行测试。

  • 写优先
#include <iostream>
#include <pthread.h>
#include <unistd.h>pthread_rwlock_t m_lock;
pthread_rwlockattr_t attr;int A = 0, B = 0;// thread1
void* threadFunc1(void* p)
{printf("thread 1 running..\n");pthread_rwlock_rdlock(&m_lock);printf("thread 1 read source A=%d\n", A);usleep(3000000); // 等待3s,此时线程2大概率会被唤醒并申请写锁pthread_rwlock_rdlock(&m_lock);printf("thread 1 read source B=%d\n", B);//释放读锁pthread_rwlock_unlock(&m_lock);pthread_rwlock_unlock(&m_lock);return NULL;
}//thread2
void* threadFunc2(void* p)
{printf("thread 2 running..\n");pthread_rwlock_wrlock(&m_lock);A = 1;B = 1;printf("thread 2 write source A and B\n");//释放写锁pthread_rwlock_unlock(&m_lock);return NULL;
}int main()
{pthread_rwlockattr_init(&attr);pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);//设置写锁优先级高//初始化读写锁if (pthread_rwlock_init(&m_lock, &attr) != 0){printf("init rwlock failed\n");return -1;}//初始化线程pthread_t hThread1;pthread_t hThread2;if (pthread_create(&hThread1, NULL, &threadFunc1, NULL) != 0){printf("create thread 1 failed\n");return -1;}usleep(1000000);if (pthread_create(&hThread2, NULL, &threadFunc2, NULL) != 0){printf("create thread 2 failed\n");return -1;}pthread_join(hThread1, NULL);pthread_join(hThread2, NULL);pthread_rwlock_destroy(&m_lock);return 0;
}

设置写优先会导致死锁。

  • 读优先
#include <iostream>
#include <pthread.h>
#include <unistd.h>pthread_rwlock_t m_lock;
pthread_rwlockattr_t attr;int A = 0, B = 0;// thread1
void* threadFunc1(void* p)
{printf("thread 1 running..\n");pthread_rwlock_rdlock(&m_lock);printf("thread 1 read source A=%d\n", A);usleep(3000000); // 等待3s,此时线程2大概率会被唤醒并申请写锁pthread_rwlock_rdlock(&m_lock);printf("thread 1 read source B=%d\n", B);//释放读锁pthread_rwlock_unlock(&m_lock);pthread_rwlock_unlock(&m_lock);return NULL;
}//thread2
void* threadFunc2(void* p)
{printf("thread 2 running..\n");pthread_rwlock_wrlock(&m_lock);A = 1;B = 1;printf("thread 2 write source A and B\n");//释放写锁pthread_rwlock_unlock(&m_lock);return NULL;
}int main()
{pthread_rwlockattr_init(&attr);pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP);//初始化读写锁if (pthread_rwlock_init(&m_lock, &attr) != 0){printf("init rwlock failed\n");return -1;}//初始化线程pthread_t hThread1;pthread_t hThread2;if (pthread_create(&hThread1, NULL, &threadFunc1, NULL) != 0){printf("create thread 1 failed\n");return -1;}usleep(1000000);if (pthread_create(&hThread2, NULL, &threadFunc2, NULL) != 0){printf("create thread 2 failed\n");return -1;}pthread_join(hThread1, NULL);pthread_join(hThread2, NULL);pthread_rwlock_destroy(&m_lock);return 0;
}

读优先则没有死锁的问题,可以正常的执行下去。

thread 1 running..
thread 1 read source A=0
thread 2 running..
thread 1 read source B=0
thread 2 write source A and B

通过上面的实验,当reader lock需要重入时,需要很谨慎,一旦读写锁的属性是写优先,那么很有可能会产生死锁。

总结

  • 读写锁适用于读多写少的场景,在这种场景下可能会有一些性能收益
  • 读写锁的使用上存在着一些陷阱,平常尽量用互斥锁(mutex)代替读写锁。

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

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

相关文章

地震勘探——相关概念(一)

地震波的基本介绍 波前&#xff1a;波在同一时刻所到达的点所构成的面&#xff0c;这个面上构成的相位是相同的。波前的形状取决于传播介质的物理性质。我们可以用地震波动方程模拟波前变化&#xff08;波场快照&#xff09;。 射线&#xff08;Ray&#xff09;&#xff1a;是…

Unity Golang教程-Shader编写一个流动的云效果

创建目录 一个友好的项目&#xff0c;项目目录结构是很重要的。我们先导入一个登录界面模型资源。 我们先创建Art表示是美术类的资源&#xff0c;资源是模型创建Model文件夹&#xff0c;由于是在登录界面所以创建Login文件夹&#xff0c;下面依次是模型对应的资源&#xff0c…

【SkyWalking】SkyWalking是如何实现跨进程传播链路数据?

文章目录 一、简介1 为什么写这篇文章2 跨进程传播协议-简介 二、协议1 Standard Header项2 Extension Header项3 Correlation Header项 三、跨进程传播协议的源码分析1 OpenTracing规范2 通过dubbo插件分析跨进程数据传播3 分析跨进程传播协议的核心源码 四、小结参考 一、简介…

C++变量默认初始化

初始化不是赋值&#xff0c;初始化是指创建变量时赋予一个初始值&#xff0c;赋值是指将变量的当前值擦除&#xff0c;赋予新值。 如果定义变量时没有初始化&#xff0c;则变量会被系统默认初始化。“默认值”取决于变量的&#xff1a;类型位置 startmindmap * C变量默认初始…

对于无法直接获取URL的数据爬虫

在爬学校安全教育题库的时候发现题库分页实际上执行了一段js代码&#xff0c;如下图所示 点击下一页时是执行了函数doPostBack&#xff0c;查看页面源码如下 点击下一页后这段js提交了一个表单&#xff0c;随后后端返回对应数据&#xff0c;一开始尝试分析获取对应两个参数&a…

【虚拟机】桥接模式下访问外网

目录 一、桥接模式的作用原理 二、配置桥接模式实现外网访问 1、设置 VMnet0 要桥接的网卡 2、虚拟机选择 VMnet0 网卡 3、手动配置虚拟机IP 一、桥接模式的作用原理 桥接模式相当于在当前局域网里创立了一个单独的主机&#xff0c;该主机桥接到宿主主机的网卡&#xff0…

细粒度特征提取和定位用于目标检测:PPCNN

1、简介 近年来&#xff0c;深度卷积神经网络在计算机视觉上取得了优异的性能。深度卷积神经网络以精确地分类目标信息而闻名&#xff0c;并采用了简单的卷积体系结构来降低图层的复杂性。基于深度卷积神经网络概念设计的VGG网络。VGGNet在对大规模图像进行分类方面取得了巨大…

uCOSIII实时操作系统 三 移植

目录 uCOSIII简介&#xff1a; 准备工作&#xff1a; 准备基础工程&#xff1a; UCOSIII工程源码&#xff1a; UCOSIII移植&#xff1a; 向基础工程中添加相应的文件夹 向工程中添加分组 常见问题&#xff1a; 下载验证&#xff1a; uCOSIII简介&#xff1a; UCOS-I…

【Nginx学习】—Nginx基本知识

【Nginx学习】—Nginx基本知识 一、什么是Nginx Nginx是一个高性能的HTTP和反向代理的web服务器&#xff0c;Nginx是一款轻量级的Web服务器/反向代理服务器处理高并发能力是十分强大的&#xff0c;并且支持热部署&#xff0c;启动简单&#xff0c;可以做到7*24不间断运行。 …

SketchyCOCO数据集进行前景图像、背景图像和全景图像的分类

SketchyCOCO数据集进行前景图像、背景图像和全景图像的分类 import os import shutildef CopyFile(src, dst, filename):if not os.path.exists(dst):os.makedirs(dst)print(create dir: dst)try:shutil.copy(src\\filename, dst\\filename)except Exception as e:print(cop…

计算机网络-计算机网络体系结构-物理层

目录 一、通信基础 通信方式 传输方式 码元 传输率 *二 准则 2.1奈氏准则(奈奎斯特定理) 2.2香农定理 三、信号的编码和调制 *数字数据->数字信号 数字数据->模拟信号 模拟数据->数字信号 模拟数据->模拟信号 *四、数据交换方式 电路交换 报文交换…

kafka客户端应用参数详解

一、基本客户端收发消息 Kafka提供了非常简单的客户端API。只需要引入一个Maven依赖即可&#xff1a; <dependency><groupId>org.apache.kafka</groupId><artifactId>kafka_2.13</artifactId><version>3.4.0</version></depend…

力扣 -- 516. 最长回文子序列

解题步骤&#xff1a; 参考代码&#xff1a; class Solution { public:int longestPalindromeSubseq(string s) {int ns.size();vector<vector<int>> dp(n,vector<int>(n));//记得从下往上填表for(int in-1;i>0;i--){//记得i是小于等于j的for(int ji;j&l…

山体滑坡监测系统——高效、便捷的新选择

在当今社会&#xff0c;科技的进步为我们的生活和工作带来了诸多便利。而在山体滑坡监测领域&#xff0c;全球导航卫星系统&#xff08;GNSS&#xff09;的引入更是增加了数据监测的高效性和便捷性。 一、山体滑坡监测系统的基本原理 山体滑坡监测系统是由监控平台和GNSS位移…

2.6 宽带接入技术

思维导图&#xff1a; 前言&#xff1a; 我的理解&#xff1a; 1. **早期互联网接入技术的局限性**&#xff1a; - 作者首先回顾了早期用户通过电话线和调制解调器连接到互联网服务提供商&#xff08;ISP&#xff09;的方式&#xff0c;指出这种方式的速度上限是56 kbit/…

UE5.1编辑器拓展【三、脚本化资产行为,删除无引用资产】

目录 需要考虑的问题 重定向的修复函数 代码&#xff1a; 删除无引用资产 代码 需要添加的头文件和模块 在我们删除资产的时候&#xff0c;会发现&#xff0c;有些资产在删除的时候会出现有被什么什么引用&#xff0c;还有的是没有被引用。 而我们如果直接选择一片去进行…

PHP 伪协议:使用 php://input 访问原始 POST 数据

文章目录 参考环境PHP 伪协议概念为什么需要 PHP 伪协议&#xff1f; php://input为什么需要 php://input&#xff1f;更灵活的数据处理减小性能压力 发送 POST 数据HackBarHackBar 插件的获取 $_POST打开 HackBar 插件通过 HackBar 插件发起 POST 请求 基操 enable_post_data_…

ROS机械臂开发-开发环境搭建【一】

目录 前言环境配置docker搭建Ubuntu环境安装ROS 基础ROS文件系统 bugs 前言 想系统学习ROS&#xff0c;做一些机器人开发。因为有些基础了&#xff0c;这里随便写写记录一下。 环境配置 docker搭建Ubuntu环境 Dockerfile # 基础镜像 FROM ubuntu:18.04 # 设置变量 ENV ETC…

[开源]基于Vue的拖拽式数据报表设计器,为简化开发提高效率而生

一、开源项目简介 Cola-Designer 是一个 基于VUE&#xff0c;实现拖拽 配置方式生成数据大屏&#xff0c;为简化开发、提高效率而生。 二、开源协议 使用GPL-2.0开源协议 三、界面展示 概览 部分截图&#xff1a; 四、功能概述 特性 0 代码 实现完全拖拽 配置式生成…

【好玩】如何在github主页放一条贪吃蛇

前言 &#x1f34a;缘由 github放小蛇&#xff0c;就问你烧不烧 起因看到大佬github上有一条贪吃蛇扭来扭去&#xff0c;觉得好玩&#xff0c;遂给大家分享一下本狗的玩蛇历程 &#x1f95d;成果初展 贪吃蛇 &#x1f3af;主要目标 实现3大重点 1. github设置主页 2. git…