Linux读写锁的容易犯的问题

news2025/1/11 7:43:09

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 maintain
     binary 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);
                // test
                pthread_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);
                //test2
                pthread_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 ./test1

real    0m2.531s
user    0m5.175s
sys     0m4.200s
[root@localhost test1]# time ./test2

real    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.coloradmin.cn/o/1068031.html

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

相关文章

机器学习算法基础--KNN分类算法

文章目录 1.KNN算法原理介绍2.KNN分类决策原则3.KNN度量距离介绍3.1.闵可夫斯基距离3.2.曼哈顿距离3.3.欧式距离 4.KNN分类算法实现5.KNN分类算法效果6.参考文章与致谢 1.KNN算法原理介绍 KNN&#xff08;K-Nearest Neighbor&#xff09;工作原理&#xff1a; 在一个存在标签的…

Stable Diffusion 图片换脸插件Roop保姆教程 附错误解决办法和API使用

换脸技术已经不是新鲜事物,但如何实现简单、快速、高效的换脸操作呢?Roop插件正是为解决这一问题而生的。 sd-webui-roop 插件适用于已经本地部署了SD的用户。相较于传统的换脸技术,Roop插件几乎不需要训练,只需一张照片,即可在10秒内完成换脸。 但是要注意到是必须注意…

Docker搭建MySQL8.0主从复制(一主一从)

0. 配置说明 宿主机使用的版本为19045的win10专业版&#xff0c;MySQL使用的是8.0&#xff0c;Docker容器使用Linux。 1. 安装Docker Desktop 略 修改Docker默认安装路径 安装包自己就提供了修改安装路径的功能&#xff0c;CMD中运行&#xff1a; “Docker Desktop Installe…

软件测试面试之问——角色扮演

作为软件测试工程师&#xff0c;在求职面试中经常会被问到这样一个问题&#xff1a;你认为测试工程师在企业中扮演着什么样的角色呢&#xff1f; 某度百科是这样概括的&#xff1a;“软件测试工程师在一家软件企业中担当的是‘质量管理’角色&#xff0c;及时发现软件问题并及…

MySql8.0 + Qt 对数据库操作 - 初窥篇1

一、开发背景 32 位程序连接 64 位的数据库实例 二、开发环境 QtCreator4.8.2 Qt5.12.2 MySql8.0.33 三、实现步骤 1、测试驱动代码 测试结果 int main(int argc, char *argv[]) {QSqlDatabase db QSqlDatabase::addDatabase("QMYSQL");db.setHostName("…

CTF之信息收集

什么是信息收集 信息收集是指通过各种方式获取所需要的信息&#xff0c;以便我们在后续的渗透过程更好的进行。最简单的比如说目标站点的IP、中间件、脚本语言、端口、邮箱等等。我觉得信息收集在我们参透测试的过程当中&#xff0c;是最重要的一环&#xff0c;这一环节没做好…

笔记01:随机过程——随机游动

一、伯努利随机过程 1. n次伯努利实验中&#xff08;x1&#xff09;发生的总次数Yn&#xff1a; (二项分布) 2. 伯努利实验中事件第一次发生的时间L1&#xff1a; &#xff08;几何分布&#xff09; 3. n次伯努利实验中事件第k次发生的时间Lk&#xff1a; &#xff08;帕斯卡分…

科技云报道:押注向量数据库,为时过早?

科技云报道原创。 在大模型的高调火热之下&#xff0c;向量数据库也获得了前所未有的关注。 近两个月内&#xff0c;向量数据库迎来融资潮&#xff0c;Qdrant、Chroma、Weaviate先后获得融资&#xff0c;Pinecone宣布1亿美元B轮融资&#xff0c;估值达到7.5亿美元。 东北证券…

小谈设计模式(22)—单例模式

小谈设计模式&#xff08;22&#xff09;—单例模式 专栏介绍专栏地址专栏介绍 单例模式点睛所在优缺点分析优点确保只有一个实例全局访问点节省资源线程安全 缺点难以扩展对象的生命周期单一职责原则隐藏依赖关系 Java程序实例实例a分析实例b&#xff0c;更安全分析优化 ——“…

Git 学习笔记 | Git 基本理论

Git 学习笔记 | Git 基本理论 Git 学习笔记 | Git 基本理论Git 工作区域Git 工作流程 Git 学习笔记 | Git 基本理论 Git 工作区域 Git本地有三个工作区域&#xff1a;工作目录&#xff08;Working Directory&#xff09;、暂存区(Stage/Index)、资源库(Repository或Git Direc…

【Kubernetes】当K8s出现问题时,我们可以从哪些方面排查出

前言 kubernetes&#xff0c;简称K8s&#xff0c;是用8代替名字中间的8个字符“ubernete”而成的缩写。是一个开源的&#xff0c;用于管理云平台中多个主机上的容器化的应用&#xff0c;Kubernetes的目标是让部署容器化的应用简单并且高效&#xff08;powerful&#xff09;,Kub…

高级 I/O【Linux】

阅读前导&#xff1a; “高级 I/O”处于知识树中网络和操作系统的最后&#xff0c;因此本文默认读者有计算机网络和操作系统的基础。 1. 什么是 I/O 下面以“流”&#xff08;stream&#xff09;和冯诺依曼体系架构的视角来简单回顾一下什么是 I/O&#xff1a; I/O可以理解…

bin-editor-next实现josn序列化

线上链接 BIN-EDITOR-NEXThttps://wangbin3162.gitee.io/bin-editor-next/#/editor gitee地址bin-editor-next: ace-editor 的vue3升级版本https://gitee.com/wangbin3162/bin-editor-next#https://gitee.com/link?targethttps%3A%2F%2Funpkg.com%2Fbin-editor-next%2F 实现…

规则引擎(JVS-rules):从应用到场景的全面解析

JVS-RULES规则引擎是一款可视化的业务规则设计器&#xff0c;它的核心功能在于可集成多种数据源&#xff0c;包括多种类型数据库和API&#xff0c;将数据进行界面可视化的加工处理后&#xff0c;形成策略节点所需要的各种变量&#xff0c;然后通过规则的可视化编排&#xff0c;…

2.3 物理层下面的传输媒体

思维导图&#xff1a; 前言&#xff1a; 2.3 物理层下的传输媒体 --- **传输媒体概述**&#xff1a; 传输媒体&#xff0c;又称传输介质或传输媒介&#xff0c;它是连接发送器与接收器之间的物理路径&#xff0c;扮演着在这两者之间传递数据的角色。 --- **分类**&#x…

李宏毅 2022机器学习 HW3 boss baseline 上分记录

作业数据是所有数据都有标签的版本。 李宏毅 2022机器学习 HW3 boss baseline 上分记录 1. 训练数据增强2. cross validation&ensemble3. test dataset augmentation4. resnet 1. 训练数据增强 结论&#xff1a;训练数据增强、更长时间的训练、dropout都证明很有效果&…

Gooxi全面拥抱AIGC时代,让智慧算力触手可及

8月31日凌晨&#xff0c;百度正式宣布文心一言全面对外开放&#xff0c;打响了国内大模型上线的第一枪&#xff0c;紧接着五家国内大模型相继对外开放。当前AIGC已经成为业内竞相追逐的热点与重构产业生产力的关键&#xff0c;在生物医药、材料分析、自动驾驶、元宇宙等领域已逐…

兼容性测试的定义和目标是什么

兼容性测试是通过在各种操作系统、浏览器、设备等多样化环境下测试软件&#xff0c;以验证其在这些环境中的稳定性和一致性。兼容性测试的目标是确保软件在不同平台上具有一致的功能、性能和用户体验&#xff0c;以满足广大用户的需求。 一、兼容性测试的定义和目标 兼容性测试…

进程间通信-信号概述

一、信号的概念 信号是一种进程间通信的机制&#xff0c;用于在软件层面上对异步事件进行通知。信号通常是由操作系统或其他进程发送给目标进程的一种通知&#xff0c;以指示发生了某种事件或条件。这样的事件可能包括硬件异常、用户输入、定时器到期、子进程状态变化等。 引发…

C# OpenCvSharp 实现迷宫解密

效果 项目 代码 using OpenCvSharp; using System; using System.Drawing; using System.Windows.Forms;namespace OpenCvSharp_实现迷宫解密 {public partial class Form1 : Form{public Form1(){InitializeComponent();}private void Form1_Load(object sender, EventArgs e…