《Linux从练气到飞升》No.27 Linux中的线程互斥

🕺作者: 主页

我的专栏
C语言从0到1
探秘C++
数据结构从0到1
探秘Linux
菜鸟刷题集

😘欢迎关注:👍点赞🙌收藏✍️留言

🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!

文章目录

    • 前言
    • 1 进程线程间的互斥相关背景概念
    • 2 线程安全
    • 3 线程不安全
      • 3.1 线程不安全(看看猪跑
    • 4 互斥量mutex
      • 4.1 为什么可能无法获得争取结果?
      • 4.2 怎么解决?
    • 5 互斥锁
      • 5.1 什么是互斥锁
      • 5.2 逻辑梳理
      • 5.3 加锁逻辑
    • 6 互斥锁的接口
      • 6.1 初始化互斥锁
      • 6.2 销毁互斥锁
      • 6.3 互斥量的加锁和解锁
      • 6.4 互斥锁的使用
    • 7 死锁
      • 7.1 死锁的定义
      • 7.2 死锁产生的条件
      • 7.3 预防死锁
      • 7.4 避免死锁
      • 7.5 死锁预防和死锁避免之间的区别
      • 7.6 避免死锁的算法

前言

当谈到多线程编程时,线程互斥是一个至关重要的概念。在多线程环境下,确保共享资源的安全访问是至关重要的,而线程互斥正是为此而设计的。通过线程互斥,我们能够确保在任意给定时间内,只有一个线程能够访问共享资源,从而避免竞态条件和数据损坏。

在本篇博客中,我们将探讨线程互斥的重要性、实现线程互斥的方法以及在实际编程中如何应用线程互斥来确保多线程程序的正确性和稳定性。通过深入了解线程互斥,我们可以更好地理解多线程编程中的关键概念,提高程序的可靠性和性能。

希望本篇博客能够帮助你更好地理解线程互斥,并为你在多线程编程中遇到的挑战提供一些思路和解决方案。让我们一起深入探讨线程互斥,为构建高效、稳定的多线程程序打下坚实的基础。

1 进程线程间的互斥相关背景概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

2 线程安全

多个线程并发同一段代码时不会出现不同的结果,多个执行流访问临界资源不会导致程序产生二义性。

  • 执行流:可以理解为线程
  • 访问:指的是对临界资源进行操作
  • 临界资源:指的是多个线程都可以访问到的资源
  • 临界区:代码操作临界资源的代码区域称之为临界区
  • 二义性:相同的代码,结果不唯一

3 线程不安全

不安全、和上面就相反喽

  1. 假设一个场景:
    假设有一个CPU,两个线程,线程A和线程B,线程A和线程B都要对全局变量i(10)进行++操作
  2. 假设线程A先运行,但是线程A将i的值读取到寄存器之后,就被线程切换了。(操作系统会保存线程A的程序计数器和上下文信息)
  3. 假设B线程运行,正常继续++操作,那么i的值在内存中就被修改增加1了
  4. 此时线程A切换回来了,怎么计算?内存中i的值是多少?
  5. 结论:此时的i最终结果还是11,明明加了两次,但是却不符合逻辑,这就是不安全
  6. 怎么解决?这就需要用到互斥了,每次只允许一个线程进入修改,这样就不会有这种情况了

6f44ffc80d154eb998e5aaf484f98d17.png

3.1 线程不安全(看看猪跑

在我们想买票的时候,如果有两个人同时下单,它会发生什么呢?票是怎么发放的,会不会有两个人买到同一张票的情况?会不会有票数为负的情况?
我们通过代码来模拟一下,如果线程不安全时,抢票的情况
代码如下:
我们让4个线程来循环获取ticket,模拟抢票

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
int ticket=1000;void* get_ticket(void* arg)
{while(1){if(ticket>0){cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl;ticket--;}else {break;}}return NULL;
}
int main()
{pthread_t tid[4];for(int i=0;i<4;i++){int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);if(ret!=0){cout<<"线程创建失败!"<<endl;}}for(int i=0;i<4;i++){pthread_join(tid[i],NULL);}cout<<"pthread_join end!"<<endl;return 0;
}

结果如下:
image.png
可以看到出现了负数的情况
image.png
甚至出现了两个线程抢到同一张票的情况

这就是所谓的线程不安全的情况

4 互斥量mutex

  • 大部分情况,线程使用的数据包都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属于单个线程,其他线程无法获得这种变量。
  • 但是很多时候,很多变量需要在线程间共享,这样的变量称为共享变量。通过数据的共享完成线程之间的交互。
  • 多个线程并发的操作共享变量会带来一些问题(就像前面代码中那样)

之前的抢票模拟,可以看到出现了负数的票,但是我们的条件中清楚的要求>0,这是为什么?

4.1 为什么可能无法获得争取结果?

  • if语句判断条件为真后,代码可以并发的切换到其他线程
  • –ticket操作本身就不是一个原子操作
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
  • --ticket操作本身就不是一个原子操作,而是对应三条汇编指令:
    • load:将共享变量ticket从内存加载到寄存器中
    • update:更新寄存器里面的值,执行-1操作
    • store:将新值,从寄存器写回共享变量ticket的内存地址

4.2 怎么解决?

要解决上面的问题需要做到三点

  1. 代码必须要有互斥行为,当代码进入到临界区执行时,不允许其他线程进入该临界区
  2. 如果多个线程同时要求执行临界区代码,并且临界区没有其他线程在执行,那么只允许一个线程进入临界区
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

要做到这三点,只需要一把锁就行了,这把锁叫做互斥锁
image.png

5 互斥锁

5.1 什么是互斥锁

  • 互斥锁的底层就是一个互斥量,而互斥量的本质就是一个计数器,这个计数器的本质就只有两种情况 0 和 1
  • 1 表示当前临界资源可以被访问
  • 0 表示当前临界资源不可以被访问

5.2 逻辑梳理

拿之前的抢票的情况来说,如果加上这个锁,当一个线程想去访问临界资源,他得先获取互斥锁,如果此时互斥锁的值为1,则说明它可以访问,反之则不能,如果它正在访问临界资源,此时有第二个线程想来访问临界资源,发现互斥锁为0,它就不能进入,只能等待互斥锁为1时才能进入访问。这就保证当前的临界资源在同一时刻只能被一个执行流访问了。
但是需要注意的是,如果多个线程访问临界资源的时候是互斥访问的属性,一定要在多个线程中进行同一把锁的加锁操作,这样每个线程在访问临界资源之前都要获取这把锁,若锁的值为1就可以访问,反之则不能访问;如果给线程A加锁,但是不给线程B加锁,就会导致线程不安全的情况。
ebbd81970a3349d286674c8ba418f532.png

5.3 加锁逻辑

加锁的时候会提前在寄存器的计数器中保存的一个值 0,而不管内存的计数器中保存的值为多少,都会将寄存器中保存到值 0 和内存计数器中保存的值进行交换,然后对寄存器中的值进行判断是否为 1 ,如果为 1 ,则能加锁,如果不为 1 ,则不能加锁。
5b6f639cab594553a10749ecc2fb39c8.png
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令
该指令的作用是把寄存器和内存单元的数据相交换
由于只有一条指令,保证了原子性
即使是多处理器平台,访问内存的总线周期也有先后
一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
现在我们把lock和unlock的伪代码给一下。
image.png

6 互斥锁的接口

6.1 初始化互斥锁

初始化互斥量有两种方法:

  1. 静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

  1. 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);

返回值及参数说明:

  • 返回值:如果初始化成功则返回0 失败则返回错误码
  • 参数 mutex:需要初始化的互斥量
  • 参数 attr:初始化互斥量的属性一般设置为NULL即可

6.2 销毁互斥锁

image.png
需要注意的是:

  • 使用静态初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁互斥量
  • 已经销毁的互斥量要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)

6.3 互斥量的加锁和解锁

  1. 阻塞加锁接口
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 如果互斥锁变量当中的计数器的值为1,调用该接口,则加锁成功,该接口调用完毕,函数返回
  • 如果互斥锁变量当中的计数器的值为0,调用该接口,则调用该接口的执行流阻塞在当前接口内部
  1. 非阻塞加锁接口
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 不管有没有加锁成功,都会返回,所以需要对加锁返回的结果进行判断,判断是否加锁,如果加锁成功,则操作临界资源,反之则需要循环获取互斥锁,直到拿到互斥锁
  1. 带有超时时间的加锁接口
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);struct timespec{time_t tv_sec;//秒long tv_nsec;//纳秒
};
  • 需要搭配循环来使用,并且判断返回值
  1. 解锁接口
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 不管是阻塞加锁/非阻塞加锁/带有超时时间的加锁,加锁成功的互斥锁都可以使用该接口进行加锁。

image.png

6.4 互斥锁的使用

  1. 什么时候使用初始化互斥锁?

先初始化互斥锁,再创建线程

  1. 什么时候使用销毁互斥锁?

在所有使用互斥锁的线程全部退出之后就可以销毁互斥锁

  1. 什么时候使用加锁?

线程访问临界资源之前进行加锁操作

  1. 什么时候使用解锁?

线程所有退出的地方进行解锁

  1. 加锁之后不解锁会发生什么?

以之前的抢票为例

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
int ticket=1000;pthread_mutex_t g_lock; //全局变量的互斥锁void* get_ticket(void* arg)
{while(1)//1位置加锁还是在2位置加锁{pthread_mutex_lock(&g_lock);//pos1if(ticket>0){cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl; //pos2ticket--;}else {//将下面解锁的注释去掉就是正确的代码//pthread_mutex_unlock(&g_lock);break;}//pthread_mutex_unlock(&g_lock);}return NULL;
}
int main()
{pthread_mutex_init(&g_lock,NULL);//初始化互斥锁pthread_t tid[4];for(int i=0;i<4;i++){int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);if(ret!=0){cout<<"线程创建失败!"<<endl;}}for(int i=0;i<4;i++){pthread_join(tid[i],NULL);}cout<<"pthread_join end!"<<endl;pthread_mutex_destroy(&g_lock);return 0;
}

结果:
image.png
我们可以看到它只获取一张票就不再往下执行了,陷入了死锁中
这是因为有一个工作线程加锁之后没有进行解锁,其他线程再次去获取锁时,互斥锁中计数器中的值还是0,就要被阻塞等待,所以加锁之后一定要记得解锁
image.png

7 死锁

前面我们讲如果不进行解锁会造成死锁现象,但是死锁是什么?现在我们就来讲讲

7.1 死锁的定义

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于一种永久等待状态。
就像下图
线程A获取到互斥锁1,线程B获取到互斥锁2时,线程A和线程B同时还想获取对方手里的锁(线程A还想获取互斥锁2,线程B还想获取互斥锁1),此时就会导致死锁
image.png
代码实现看一下

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;pthread_mutex_t lock1;
pthread_mutex_t lock2;void* ThreadNum1(void* args)
{(void*)args;pthread_mutex_lock(&lock1);sleep(3);pthread_mutex_lock(&lock2);return NULL;
}
void* ThreadNum2(void* args)
{(void*)args;pthread_mutex_lock(&lock2);sleep(3);pthread_mutex_lock(&lock1);return NULL;
}
int main()
{pthread_mutex_init(&lock1,NULL);pthread_mutex_init(&lock2,NULL);pthread_t tid;int ret = pthread_create(&tid,NULL,ThreadNum1,NULL);if(ret < 0){cout<<"thread1 create failed"<<endl;return 0;}ret = pthread_create(&tid,NULL,ThreadNum2,NULL);if(ret < 0){cout<<"thread2 create failed"<<endl;return 0;}while(1){;}pthread_mutex_destroy(&lock1);pthread_mutex_destroy(&lock2);return 0;
}

结果:
image.png
image.png
可以看到发生了死锁的现象

7.2 死锁产生的条件

死锁的生成有四个必要的条件:

  • 互斥条件:一个资源只能被一个执行流使用。(一个互斥锁只能被一个执行流在同一时刻拥有)
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(就像前面的例子那样,各自都是各自所需要的资源,但是都不放手)
  • 不剥夺条件:一个执行流已获得的资源在未使用完之前不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。(互相等待,无限循环)

7.3 预防死锁

想要预防死锁只要破坏死锁4个条件中的一个即可

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

7.4 避免死锁

它和死锁预防的差别很小,可以把它理解为死锁预防的一种特例。
死锁避免策略在允许三个必要条件存在的条件下,来确保永远不会达到死锁点。
死锁避免方法有:

  • 若一个进程的请求会导致死锁,那么不启动该进程。
  • 若一个进程增加的资源请求会导致死锁,则不允许这个资源的分配。

相比死锁预防策略,死锁避免策略并发性更强。但是在使用中也有诸多限制:

  • 必须事先声明每个进程请求的最大资源
  • 分配的资源数量必须是固定的
  • 在占有资源时,进程不能够退出
  • 所讨论的进程的执行顺序必须没有任何同步要求的限制

7.5 死锁预防和死锁避免之间的区别

编号比较项预防死锁避免死锁
1概念预防死锁至少阻止了发生死锁的必要条件之一。避免死锁确保系统不会进入不安全状态
2资源请求预防死锁所有的资源都是一起请求的。资源请求是根据可用的安全路径完成的。
3所需信息预防死锁不需要关于现有资源、可用资源和资源请求的信息避免死锁需要关于现有资源、可用资源和资源请求的信息
4过程通过限制资源请求过程和资源处理来防止死锁。避免死锁会自动考虑请求并检查它是否对系统安全。
5抢占有时,抢占会更频繁地发生。避免死锁在死锁避免中没有抢占。
6资源分配策略用于防止死锁的资源分配策略是保守的。防止死锁的资源分配策略并不保守。
7未来的资源请求预防死锁不需要知道未来的进程资源请求。避免死锁需要了解未来的进程资源请求。
8优点预防死锁不涉及任何成本,因为它只需使条件之一为假,这样就不会发生死锁。由于此方法动态工作以分配资源,因此没有系统未充分利用。
9缺点死锁预防设备利用率低。避免死锁会使进程阻塞太久。
10使用示例假脱机和非阻塞同步算法。使用银行家和安全算法。

7.6 避免死锁的算法

  • 死锁检测算法:推荐博客《死锁的处理策略——检测和解除》
  • 银行家算法:推荐博客《银行家算法及其代码实现》

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

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

相关文章

Unity如何保存场景,如何导出工程文件/如何查看保存位置?【各版本通用】

如何保存场景&#xff1f; 在unity中CtrlS 或者File—>Save 输入你要保存的场景名【建议保存在Scenes文件夹下】 下图&#xff0c;保存场景不在Scenes文件夹下&#xff1a; 下图&#xff0c;保存在Scenes文件夹下&#xff1a; 下图&#xff0c;保存完成 如何导出工程文…

浅了解下:运营商大数据如何挖掘电销同行网站,APP,精准获客 ?

今天我们要讲的是运营商精准大数据营销。运营商精准大数据营销只是精准营销的一种&#xff0c;精准营销筛选包含了电话营销这个词。那么电话营销如何通过运营商大数据找到精准的客户&#xff1f;电销如何通过大数据找到准确的客户来源&#xff1f; 在全网时代&#xff0c;大数…

DeCLIP 论文阅读

DeCLIP:supervision exists everywhere:a data efficient contrastive language-image pre-training paradigm 贡献&#xff1a; 论文是为了充分利用单模态和多模态&#xff0c;充分利用单模态特征用自监督&#xff08;SIMSAM和MLM&#xff09;&#xff0c;多模态用图像文本对…

修改ubuntu终端目录背景颜色

Ubuntu终端上有部分目录是黄绿色底色&#xff0c;看着很不舒服。如下图所示&#xff1a; 这是由于修改用户权限导致的问题。 通过下面指令可以看到 echo $LS_COLORS | grep "ow" ​ 可以看到ow的默认参数是34:42ow:OTHER_WRITABLE&#xff0c;即其他用户可写权限 …

京东数据挖掘(京东数据采集):2023年Q3电脑行业数据分析报告

近年来&#xff0c;在远程办公、远程教育等需求的刺激下&#xff0c;电脑的销售增长较为显著。不过&#xff0c;随着市场的成熟乃至饱和&#xff0c;电脑销售市场也逐渐出现增长困难、需求疲软等问题。 2023年第三季度&#xff0c;电脑市场的出货量同比下滑。根据鲸参谋电商数据…

pycharm安装库失败

项目场景 pycharm安装第三方库 问题描述 python 安装第三方库总是安装失败 原因分析&#xff1a; 提示&#xff1a;这里填写问题的分析&#xff1a; 1.网络 2.网墙 解决方案&#xff1a; 加个镜像 –trusted-host mirrors.aliyun.com

JavaScript库:jQuery,简化编程

jQuery介绍 官方网站: https://jquery.com jQuery 是一个 JavaScript 库 。极大地简化了 JavaScript 编程&#xff0c;例如 JS 原生代码几十行 实现的功 能&#xff0c; jQuery 可能一两行就可以实现&#xff0c;因此得到前端程序猿广泛应用。&#xff08;现在处在比较边…

swagger精度丢失,postman调用正常,dameng数据库,long类型字段

问题出现 我们目前在迁移环境&#xff0c;然后往另带一个公司提供的框架里面迁移&#xff0c;然后就出现了很多问题&#xff0c;一个问题是我们返回的某个列表数据&#xff0c;在使用postman 的时候调用正常&#xff0c;但是当前端在制作页面的时候出现问题&#xff0c;并且sw…

【postgresql】CentOS7 安装Pgweb

Pgweb Pgweb是PostgreSQL的一个基于web的数据库浏览器&#xff0c;用Go编写&#xff0c;可在Mac、Linux和Windows机器上运行。以零依赖性的简单二进制形式分布。非常易于使用&#xff0c;并具有适当数量的功能。简单的基于web和跨平台的PostgreSQL数据库浏览器。 特点 跨平台…

JavaScript概述

一、JavaScript简介&#xff1a; JavaScript是互联网上流行的脚本语言&#xff0c;可用于HTML和web&#xff0c;可广泛应用于服务器、PC、笔记本、平板电脑和智能手机等设备。 JavaScript是一种轻量级的编程语言&#xff0c;可插入HTML页面的编程代码&#xff0c;插入HTML页面后…

陪诊小程序|陪诊系统打开陪护行业新世界

随着社会老龄化加剧&#xff0c;以及人们对于医疗服务质量的要求提高&#xff0c;陪诊服务逐渐成为了医疗体系中不可或缺的一部分。而陪诊小程序作为陪诊服务的线上平台&#xff0c;更是受到了广泛的关注。下面小编就给大家讲解下陪诊小程序的功能并阐述其系统优势。 陪诊小程序…

北大腾讯打造多模态15边形战士!语言作“纽带”,拳打脚踢各模态,超越Imagebind

AI4Happiness 投稿 量子位 | 公众号 QbitAI 北大联合腾讯打造了一个多模态15边形战士&#xff01; 以语言为中心&#xff0c;“拳打脚踢”视频、音频、深度、红外理解等各模态。 具体来说&#xff0c;研究人员提出了一个叫做LanguageBind的多模态预训练框架。 用语言作为与其…