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.rhkb.cn/news/151719.html

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

相关文章

腾讯云轻量和CVM有啥区别?怎么选择服务器配置?

腾讯云轻量服务器和云服务器有什么区别&#xff1f;为什么轻量应用服务器价格便宜&#xff1f;是因为轻量服务器CPU内存性能比云服务器CVM性能差吗&#xff1f;轻量应用服务器适合中小企业或个人开发者搭建企业官网、博客论坛、微信小程序或开发测试环境&#xff0c;云服务器CV…

关于Jupyter markdown的使用

一级标题 #空格 标题1 二级标题 ## 空格 标题2 三级标题 ###空格 标题3 无序&#xff1b; 有序&#xff1a; 数学符号&#xff1a;

记一次问题排查

1785年&#xff0c;卡文迪许在实验中发现&#xff0c;把不含水蒸气、二氧化碳的空气除去氧气和氮气后&#xff0c;仍有很少量的残余气体存在。这种现象在当时并没有引起化学家的重视。 一百多年后&#xff0c;英国物理学家瑞利测定氮气的密度时&#xff0c;发现从空气里分离出来…

Visual Studio 2019中的安全问题

最近&#xff0c;在使用Visual Studio 2019的时候遇到了一个很奇怪的问题&#xff0c;如下所示。 这里一直在说scanf函数不安全&#xff0c;导致报错&#xff0c;然后上网查了查相关资料&#xff0c;发现在代码中加那么一句就可以了&#xff0c;而且必须放在最前面。 #define …

网工内推 | IT高级运维工程师,周末双休,包吃包住,14-20k

01 深圳朗特智能控制股份有限公司 招聘岗位&#xff1a;IT高级运维工程师 职责描述&#xff1a; 1、对集团网络基础架构的建设、运维、安全制定相关标准和准则&#xff1b; 2、负责集团数据中心、核心设备、信息安全的管理和运维&#xff1b; 3、执行网络、服务器、核心交换机…

wpf中prism框架

安装prism包&#xff1a; 添加引用 using System; using System.Collections.Generic; using System.Configuration; using System.Data; using System.Linq; using System.Threading.Tasks; using System.Windows; using Prism.DryIoc; using Prism.Ioc;namespace PrismDemo …

A (1087) : DS单链表--类实现

Description 用C语言和类实现单链表&#xff0c;含头结点 属性包括&#xff1a;data数据域、next指针域 操作包括&#xff1a;插入、删除、查找 注意&#xff1a;单链表不是数组&#xff0c;所以位置从1开始对应首结点&#xff0c;头结点不放数据 类定义参考 #include<…

【考研复习】union有关的输出问题

文章目录 遇到的问题正确解答拓展参考文章 遇到的问题 首次遇到下面的代码时&#xff0c;感觉应该输出65,323。深入理解union的存储之后发现正确答案是&#xff1a;67,323. union {char c;int i; } u; int main(){u.c A;u.i 0x143;printf("%d,%d\n", u.c, u.i); …

初阶数据结构(四)带头双向链表

&#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;数据结构 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &#x1f339;&#x1f339;&#x1f339;关注我带你学习编程知识 带头双向链表 链表的相关介绍初始化链表销毁链…

XCode打包IOS应用发布App Store和Ad Hoc测试

文章目录 零、前置说明一、创建本地证书二、配置描述文件2.1 配置certificates2.1.1 配置证书2.1.2 安装cer证书2.1.2.1 打包机器和生成证书同机器2.1.2.2 打包机器和生成证书不同机器 2.2 创建Identifiers2.3 配置Devices2.4 配置Profiles2.4.1 配置生产Profile2.4.2 配置开发…

竞赛 机器视觉的试卷批改系统 - opencv python 视觉识别

文章目录 0 简介1 项目背景2 项目目的3 系统设计3.1 目标对象3.2 系统架构3.3 软件设计方案 4 图像预处理4.1 灰度二值化4.2 形态学处理4.3 算式提取4.4 倾斜校正4.5 字符分割 5 字符识别5.1 支持向量机原理5.2 基于SVM的字符识别5.3 SVM算法实现 6 算法测试7 系统实现8 最后 0…

【研究的艺术】通读《The Craft of Research》

通读《The Craft of Research》 前言1. 跟读者建立联系2. 明白问题的重要性3. 组织论述4. 论点4.1 Making Claims4.2 Assembling Reasons and Evidence4.3 Acknowledgments and Responses4.4 Warrants 未完待续。。。 前言 本篇博客是《The Craft of Research》的通读笔记&…

华为云云耀云服务器L实例评测|Elasticsearch的Docker版本的安装和参数设置 端口开放和浏览器访问

前言 最近华为云云耀云服务器L实例上新&#xff0c;也搞了一台来玩&#xff0c;期间遇到各种问题&#xff0c;在解决问题的过程中学到不少和运维相关的知识。 本篇博客介绍Elasticsearch的Docker版本的安装和参数设置&#xff0c;端口开放和浏览器访问。 其他相关的华为云云…

爱普生LQ1900KIIH复位方法

爱普生EPSON 1900KIIH是一部通用针式打印机&#xff0c;136列&#xff08;10cpi下&#xff09;的打印宽度&#xff0c;缓冲区128KB&#xff0c;打印速度为270字/秒。 打印机类型 打印方式&#xff1a;24针击打式点阵打印、打印方向&#xff1a;双向逻辑查找、安全规格标准&am…

jvm概述

1、JVM体系结构 2、JVM运行时数据区 3、JVM内存模型 JVM运行时内存 共享内存区 线程内存区 3.1、共享内存区 共享内存区 持久带(方法区 其他) 堆(Old Space Young Space(den S0 S1)) 持久代&#xff1a; JVM用持久带&#xff08;Permanent Space&#xff09;实现方法…

Aurora中的策略模式和模板模式

Aurora中的策略模式和模板模式 在aurora中为了方便以后的扩展使用了策略模式和模板模式实现图片上传和搜索功能&#xff0c;能够在配置类中设置使用Oss或者minio上传图片&#xff0c;es或者mysql文章搜索。后续有新的上传方式或者搜索方式只需要编写对应的实现类即可&#xff…

在原生html中使用less

引入less <link rel"stylesheet/less" href"./lessDemo.less" /><script src"./js/less.min.js"></script> less.min.js文件下载地址:https://github.com/less/less.js 注意&#xff1a;less文件在前&#xff0c;js文件在后…

深入理解强化学习——强化学习的基础知识

分类目录&#xff1a;《深入理解强化学习》总目录 在机器学习领域&#xff0c;有一类任务和人的选择很相似&#xff0c;即序贯决策&#xff08;Sequential Decision Making&#xff09;任务。决策和预测任务不同&#xff0c;决策往往会带来“后果”&#xff0c;因此决策者需要为…

C++ day2

自己封装一个矩形类(Rect)&#xff0c;拥有私有属性:宽度(width)、高度(height)&#xff0c; 定义公有成员函数: 初始化函数:void init(int w, int h) 更改宽度的函数:set_w(int w) 更改高度的函数:set_h(int h) 输出该矩形的周长和面积函数:void show() #include <ios…

2.4 turtle语法元素分析 | Python语言程序设计(嵩天)

文章目录 课程简介第二章 Python基本图形绘制2.4 turtle语法元素分析2.4.1 库引用与import2.4.2 turtle画笔控制函数2.4.3 turtle运动控制函数2.4.4 turtle方向控制函数2.4.5 循环语句与range()函数&#xff08;基本循环语句&#xff09;2.4.6 "Python蟒蛇绘制"代码分…