【Linux】死锁、读写锁、自旋锁

文章目录

  • 1. 死锁
    • 1.1 概念
    • 1.2 死锁形成的四个必要条件
    • 1.3 避免死锁
  • 2. 读者写者问题与读写锁
    • 2.1 读者写者问题
    • 2.2 读写锁的使用
    • 2.3 读写策略
  • 3. 自旋锁
    • 3.1 概念
    • 3.2 原理
    • 3.3 自旋锁的使用
    • 3.4 优点与缺点

在这里插入图片描述

1. 死锁

1.1 概念

死锁是指在⼀组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用且不会释放的资源而处于的⼀种永久等待状态。

在这里插入图片描述
所以,可能会造成死锁的局面
在这里插入图片描述

1.2 死锁形成的四个必要条件

  1. 互斥条件:⼀个资源每次只能被⼀个执行流使用
  2. 请求与保持条件:⼀个执行流因请求资源而阻塞时,对已获得的资源保持不放在这里插入图片描述
  3. 不剥夺条件:⼀个执行流已获得的资源,在未使用完之前,不能强行剥夺
    在这里插入图片描述
  4. 循环等待条件:若干执行流之间形成⼀种头尾相接的循环等待资源的关系
    在这里插入图片描述

1.3 避免死锁

方式:破坏死锁的四个必要条件。

互斥条件:

  • 破坏难度:由于互斥性是资源访问的基本特性,因此很难或不应该被破坏。

请求和保持条件:

  • 破坏策略:可以采用静态分配策略,即进程在运行前一次性申请完它所需要的全部资源,在资源未满足前,它不启动。这样就不会出现占有资源又等待其他资源的情况,从而破坏请求和保持条件。

不剥夺条件

  • 破坏策略:可以采用剥夺式调度策略,即当一个进程申请新资源而得不到满足时,可以释放它所占有的部分资源,以便其他进程使用,从而破坏不剥夺条件。

循环等待条件

  • 破坏策略:可以采用顺序资源分配法,即首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,只能申请编号比之前大的资源。这样可以避免形成循环等待链,从而破坏循环等待条件。

例如:破环请求与保持条件,使资源⼀次性分配。

lock函数可确保所传递的锁对象全部获取成功,本质就是先申请一把锁,在申请的锁种再申请提供的锁对象,因为申请一把锁的操作是原子的。

在这里插入图片描述

#include <iostream>
#include <mutex>
#include <unistd.h>
// 定义两个共享资源(整数变量)和两个互斥锁
int shared_resource1 = 0;
int shared_resource2 = 0;
std::mutex mtx1, mtx2;
// ⼀个函数,同时访问两个共享资源
void access_shared_resources()
{
	std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
	std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
	// // 使⽤ std::lock 同时锁定两个互斥锁
	std::lock(lock1, lock2);
	// 现在两个互斥锁都已锁定,可以安全地访问共享资源
	int cnt = 10000;
	while (cnt)
	{
		++shared_resource1;
		++shared_resource2;
		cnt--;
	} 
	// 当离开 access_shared_resources 的作⽤域时,lock1 和 lock2 的析构函数会被⾃动调⽤
	// 这会导致它们各⾃的互斥量被⾃动解锁
} 

2. 读者写者问题与读写锁

2.1 读者写者问题

读者写者问题其实与生产者消费者问题类似,都是多线程之间互相同步的一种策略。

例如我们在写博客的时候,在我写的时候你是看不到的,直到我发布出去,你才能看到;与此同时在你看的时候,可能还有很多人都在看。

读者写者问题也应该遵循“321"原则:3种关系,2种角色,1个交易场所。

三种关系如下:

  • 写者与写者之间互斥,即一个写者在修改数据时,其他写者不能访问。
  • 读者与写者之间互斥&&同步,即不能同时有一个线程在读,而另一个线程在写。
    • 当一个读者申请进行读操作时,如果已有写者在访问共享资源,则该读者必须等到没有写者访问后才能开始读操作。
    • 当一个写者申请进行写操作时,如果已有读者正在读取数据,写者必须等待所有读者完成读取后才能开始写操作。
  • 读者之间可以并发(即没关系),即可以有一个或多个读者在读。

读者写者 vs 生成者消费者

二者最大的区别就是:消费者会“取走”共享资源,而读者不会。

伪代码理解读者写者的逻辑:

在这里插入图片描述

2.2 读写锁的使用

初始化:

  • int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
  • pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

销毁:

  • int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁:

  • 读者加锁int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  • 写者加锁int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

解锁:

  • int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
#include <iostream>
#include <unistd.h>

#include <pthread.h>
#include <sys/types.h>

pthread_rwlock_t rwlock; // 读写锁
static int g_data = 0; // 共享资源

pthread_mutex_t lockScreen;

void *Reader(void *args)
{
    int id = *(int *)args;

    while (true)
    {
        pthread_rwlock_rdlock(&rwlock); // 读者加锁

        pthread_mutex_lock(&lockScreen);
        std::cout << "读者-" << id << " 正在读取数据, 数据是: " << g_data << std::endl;
        pthread_mutex_unlock(&lockScreen);
        sleep(2);
        pthread_rwlock_unlock(&rwlock); // 读者解锁
        sleep(1);
    }
    delete (int *)args;
    return nullptr;
}

void *Writer(void *args)
{
    int id = *(int *)args;
    while (true)
    {
        pthread_rwlock_wrlock(&rwlock); // 写者加锁
        g_data += 2;                    // 修改共享数据

        pthread_mutex_lock(&lockScreen);
        std::cout << "写者- " << id << " 正在写入. 新的数据是: " << g_data << std::endl;
        pthread_mutex_unlock(&lockScreen);
        sleep(1);
        pthread_rwlock_unlock(&rwlock); // 解锁
        sleep(1);

    }
    delete (int *)args;
    return NULL;
}

int main()
{
    pthread_rwlock_init(&rwlock, nullptr); // 初始化读写锁
    pthread_mutex_init(&lockScreen,nullptr);
    const int reader_num = 5; // 读者数量
    const int writer_num = 10;  // 写者数量
    const int total = reader_num + writer_num;
    pthread_t threads[total];

    for (int i = 0; i < reader_num; i++)
    {
        int *id = new int(i);
        pthread_create(&threads[i], 0, Reader, id);
    }

    for (int i = reader_num; i < total; i++)
    {
        int *id = new int(i - reader_num);
        pthread_create(&threads[i], 0, Writer, id);
    }

    for (int i = 0; i < total; i++)
    {
        pthread_join(threads[i], nullptr);
    }
    return 0;
}

在这里插入图片描述

2.3 读写策略

  • 读者优先
    在这种策略中, 系统会尽可能多地允许多个读者同时访问资源(比如共享文件或数据) , 而不会优先考虑写者。 这意味着当有读者正在读取时, 新到达的读者会立即被允许进入读取区, 而写者则会被阻塞, 直到所有读者都离开读取区。 读者优先策略可能会导致写者饥饿(即写者长时间无法获得写入权限), 特别是当读者频繁到达时。
  • 写者优先
    在这种策略中, 系统会优先考虑写者。 当写者请求写入权限时, 系统会尽快地让写者进入写入区, 即使此时有读者正在读取。 这通常意味着一旦有写者到达, 所有后续的读者都会被阻塞, 直到写者完成写入并离开写入区。 写者优先策略可以减少写者等待的时间, 但可能会导致读者饥饿(即读者长时间无法获得读取权限) , 特别是当写者频繁到达时。

选择合适的策略时,需要根据具体的应用场景和需求进行权衡。例如,在需要频繁读取而写入较少的应用中,读者优先策略可能更为合适;而在需要频繁写入的应用中,写者优先策略可能更为合适。

3. 自旋锁

3.1 概念

在我们之前讲的信号量或互斥锁,都有一个特点:申请锁失败,申请线程都要阻塞挂起等待。

但是,当一个线程在临界区内执行的时长非常短时,那么等待线程阻塞、挂起、唤醒的代价是比较大的。所以有一种锁在申请临界区的时候,可以不阻塞等待,它会持续自旋(即在一个循环中不断检查锁是否可用),这种状态的锁我们称之为自旋锁

这种机制减少了线程切换的开销, 适用于短时间内锁的竞争情况

3.2 原理

自旋锁通常使用一个共享的标志位(如一个布尔值)来表示锁的状态。 当标志位为true 时,表示锁已被某个线程占用;当标志位为 false 时,表示锁可用。 当一个线程尝试获取自旋锁时,它会不断检查标志位:

  • 如果标志位为 false,表示锁可用, 线程将设置标志位为 true, 表示自己占用了锁, 并进入临界区。
  • 如果标志位为 true(即锁已被其他线程占用),线程会在一个循环中不断自旋等待, 直到锁被释放

上面检测标志位的操作一定是原子性的。

伪代码理解原理:
在这里插入图片描述

3.3 自旋锁的使用

Linux 提供的自旋锁系统调用

  • 初始化:
    int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

在这里插入图片描述

  • 销毁:
    int pthread_spin_destroy(pthread_spinlock_t *lock);

  • 加锁
    int pthread_spin_lock(pthread_spinlock_t *lock);
    int pthread_spin_trylock(pthread_spinlock_t *lock); 申请锁失败就返回,就可以使用适当的退避策略了。

  • 解锁:
    int pthread_spin_unlock(pthread_spinlock_t *lock);

使用:

#include <iostream>
#include <unistd.h>
#include <pthread.h>

pthread_spinlock_t spinlock;    //自旋锁

static int g_ticket = 5000;

void* func(void* args)
{
    char* name = (char*)args;
    while(true)
    {
        pthread_spin_lock(&spinlock);
        if(g_ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name, g_ticket);
            g_ticket--;
            pthread_spin_unlock(&spinlock);
        }
        else
        {
            pthread_spin_unlock(&spinlock);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_spin_init(&spinlock,PTHREAD_PROCESS_PRIVATE);	//初始化
    pthread_t t1,t2,t3,t4,t5;
    pthread_create(&t1,nullptr,func,(void*)"thread-1");
    pthread_create(&t2,nullptr,func,(void*)"thread-2");
    pthread_create(&t3,nullptr,func,(void*)"thread-3");
    pthread_create(&t4,nullptr,func,(void*)"thread-4");
    pthread_create(&t5,nullptr,func,(void*)"thread-5");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);
    pthread_join(t5,nullptr);

    pthread_spin_destroy(&spinlock);	//销毁
    return 0;
}

3.4 优点与缺点

优点

  • 低延迟: 自旋锁适用于短时间内的锁竞争情况, 因为它不会让线程进入休眠状态, 从而避免了线程切换的开销, 提高了锁操作的效率。
  • 减少系统调度开销: 等待锁的线程不会被阻塞, 不需要上下文切换, 从而减少了系统调度的开销。

缺点

  • CPU 资源浪费: 如果锁的持有时间较长,等待获取锁的线程会一直循环等待,导致 CPU 资源的浪费。
  • 可能引起活锁: 当多个线程同时自旋等待同一个锁时, 如果没有适当的退避策略, 可能会导致所有线程都在不断检查锁状态而无法进入临界区, 形成活锁

使用场景

  • 短暂等待的情况: 适用于锁被占用时间很短的场景, 如多线程对共享数据进行简单的读写操作。
  • 多线程锁使用: 通常用于系统底层, 同步多个 CPU 对共享资源的访问。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

单片机学习笔记 15. 串口通信(理论)

更多单片机学习笔记&#xff1a;单片机学习笔记 1. 点亮一个LED灯单片机学习笔记 2. LED灯闪烁单片机学习笔记 3. LED灯流水灯单片机学习笔记 4. 蜂鸣器滴~滴~滴~单片机学习笔记 5. 数码管静态显示单片机学习笔记 6. 数码管动态显示单片机学习笔记 7. 独立键盘单片机学习笔记 8…

MyBatis异常体系中ErrorContext和ExceptionFactory原理分析

&#x1f3ae; 作者主页&#xff1a;点击 &#x1f381; 完整专栏和代码&#xff1a;点击 &#x1f3e1; 博客主页&#xff1a;点击 文章目录 exceptions包分包设计ExceptionFactory类介绍为什么使用工厂不是直接new呢&#xff1f;【统一的异常处理机制】【异常的封装与转化】【…

【Canvas与雷达】点鼠标可暂停金边蓝屏雷达显示屏

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>点鼠标可暂停金边蓝屏雷达显示屏 Draft1</title><style typ…

Spark Optimization —— Reducing Shuffle

Spark Optimization : Reducing Shuffle “Shuffling is the only thing which Nature cannot undo.” — Arthur Eddington Shuffle Shuffle Shuffle I used to see people playing cards and using the word “Shuffle” even before I knew how to play it. Shuffling in c…

数据结构 (22)哈夫曼树及其应用

前言 哈夫曼树&#xff08;Huffman Tree&#xff09;&#xff0c;又称最优二叉树或最优树&#xff0c;是一种特殊的二叉树结构&#xff0c;其带权路径长度&#xff08;WPL&#xff09;最短。 一、哈夫曼树的基本概念 定义&#xff1a;给定N个权值作为N个叶子结点&#xff0c;构…

Android Studio安装TalkX AI编程助手

文章目录 TalkX简介编程场景 TalkX安装TalkX编程使用ai编程助手相关文章 TalkX简介 TalkX是一款将OpenAI的GPT 3.5/4模型集成到IDE的AI编程插件。它免费提供特定场景的AI编程指导&#xff0c;帮助开发人员提高工作效率约38%&#xff0c;甚至在解决编程问题的效率上提升超过2倍…

泷羽sec-shell脚本(全) 学习笔记

声明&#xff01; 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&a…

开发者如何使用GCC提升开发效率IMG操作

看此篇前请先阅读https://blog.csdn.net/qq_20330595/article/details/144134160?spm1001.2014.3001.5502 stb_image库配置 https://github.com/nothings/stb 代码 #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #define STB_IMAGE_WRITE_IMPLEM…

vue3【实战】面包屑【组件封装】Breadcrumb (根据菜单自动生成,实时响应路由变化,添加顺滑的过渡动画)

效果预览 技术方案 vue3 ( vite | TS | vueUse | AutoImport ) Element Plus UnoCSS 技术要点 根据当前路由查询所有父级路由 /*** 从树状列表中获取指定节点的所有父节点** param treeList 树状列表&#xff0c;包含多个节点* param value 目标节点的路径值* param parents…

pdf也算是矢量图——pdf大小调整--福昕pdf

有时候需要把pdf作为矢量图放到latex论文中&#xff0c;有时候需要裁剪掉空白的部分&#xff0c;就需要用福昕pdf进行编辑&#xff0c; 参考文章&#xff1a;福昕高级PDF编辑器裁切工具怎么用&#xff1f;裁切工具使用方法介绍_福昕PDF软件工具集 (foxitsoftware.cn)

【k8s】kubelet 的相关证书

在 Kubernetes 集群中&#xff0c;kubelet 使用的证书通常存放在节点上的特定目录。这些证书用于 kubelet 与 API 服务器之间的安全通信。具体的位置可能会根据你的 Kubernetes 安装方式和配置有所不同&#xff0c;下图是我自己环境【通过 kubeadm 安装的集群】中的kubelet的证…

Java项目Docker部署

docker将应用程序与该程序的依赖打包在一个文件里。运行这个文件就会生成一个虚拟容器&#xff0c;就不用担心环境问题&#xff0c;还可以进行版本管理、复制修改等。 docker安装 由于在CentOS下安装docker最常用&#xff0c;所以以Linux环境安装为主 1.安装工具包 缺少依赖…

【数据结构与算法】排序算法(上)——插入排序与选择排序

文章目录 一、常见的排序算法二、插入排序2.1、直接插入排序2.2、希尔排序( 缩小增量排序 ) 三、选择排序3.1、直接选择排序3.2、堆排序3.2.1、堆排序的代码实现 一、常见的排序算法 常见排序算法中有四大排序算法&#xff0c;第一是插入排序&#xff0c;二是选择排序&#xff…

Flink四大基石之Time (时间语义) 的使用详解

目录 一、引言 二、Time 的分类及 EventTime 的重要性 Time 分类详述 EventTime 重要性凸显 三、Watermark 机制详解 核心原理 Watermark能解决什么问题,如何解决的? Watermark图解原理 举例 总结 多并行度的水印触发 Watermark代码演示 需求 代码演示&#xff…

虚拟机docker记录

最近看了一个up的这个视频&#xff0c;感觉docker真的挺不错的&#xff0c;遂也想来搞一下&#xff1a; https://www.bilibili.com/video/BV1QC4y1A7Xi/?spm_id_from333.337.search-card.all.click&vd_sourcef5fd730321bc0e9ca497d98869046942 这里我用的是vmware安装ubu…

[ACTF2020 新生赛]BackupFile--详细解析

信息搜集 让我们寻找源文件&#xff0c;目录扫描&#xff1a; 找到了/index.php.bak文件&#xff0c;也就是index.php的备份文件。 后缀名是.bak的文件是备份文件&#xff0c;是文件格式的扩展名。 我们访问这个路径&#xff0c;就会直接下载该备份文件。 我们把.bak后缀删掉…

AD单通道AD多通道

AD单通道接线图 滑动变阻器的内部结构 左边和右边的两个引脚接的是电阻的两个固定端&#xff0c;中间这个引脚接的是滑动抽头&#xff0c;电位器外边这里有个十字形状的槽可以拧&#xff0c;往左拧&#xff0c;抽头就往左靠&#xff0c;往右拧&#xff0c;抽头就往右靠。所以外…

解决Ubuntu DNS覆盖写入127.0.0.53

ubuntu22.04解析网址时报错如图所示&#xff1a; 因为/etc/resolve.conf中存在 nameserver 127.0.0.53回环地址造成循环引用 原因&#xff1a; ubuntu17.0之后特有&#xff0c;systemd-resolvd服务会一直覆盖 解决方法&#xff1a; 1、修改resolv.config文件中的nameserver…

hint: Updates were rejected because the tip of your current branch is behind!

问题 本地仓库往远段仓库推代码时候提示&#xff1a; error: failed to push some refs to 192.168.2.1:java-base/java-cloud.git hint: Updates were rejected because the tip of your current branch is behind! refs/heads/master:refs/heads/master [rejected] (…

[golang][MAC]Go环境搭建+VsCode配置

一、go环境搭建 1.1 安装SDK 1、下载go官方SDK 官方&#xff1a;go 官方地址 中文&#xff1a;go 中文社区 根据你的设备下载对应的安装包&#xff1a; 2、打开压缩包&#xff0c;根据引导一路下一步安装。 3、检测安装是否完成打开终端&#xff0c;输入&#xff1a; go ve…