【linux线程(二)】线程互斥与线程同步

💓博主CSDN主页:杭电码农-NEO💓

⏩专栏分类:Linux从入门到精通⏪

🚚代码仓库:NEO的学习日记🚚

🌹关注我🫵带你学更多操作系统知识
  🔝🔝


在这里插入图片描述

Linux线程

  • 1. 前言
  • 2. 多线程互斥相关背景概念
  • 3. 多线程互斥详解
  • 4. 互斥锁的接口使用
  • 5. 死锁相关概念
  • 6. 线程安全和可重入的关系
  • 7. 线程同步基本概念
  • 8. 线程同步的接口使用
  • 9. 总结以及拓展

1. 前言

如果你不了解线程的基本概念,请你先
移步上一篇文章: 线程基本概念

本章重点:

本篇文章着重讲解线程互斥以及线程
同步的相关概念,以及如何实现它们.
周边概念包括临界资源,原子性,互斥量
等也会在本文当中提及


2. 多线程互斥相关背景概念

在学习互斥前,需要先补充一些相关概念:

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

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

比如说我们最常见的高铁售票系统,可以把买票操作分为三步: 第一步: 判断现在还有无车票.第二步: 乘客付款后,钱包金额减少. 第三步: 乘客获得一张车票,高铁的总票数减一.多个执行流执行这三步时可能会出现问题,如下图:

在这里插入图片描述

可以写一段代码来验证上面的情况:

// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{
	char *id = (char*)arg;
	while ( 1 ) {
		if ( ticket > 0 ) {
			usleep(1000);
			printf("%s sells ticket:%d\n", id, ticket);
			ticket--;
		} else {
		break;
		}
	}
}
int main( void )
{
	pthread_t t1, t2, t3, t4;
	pthread_create(&t1, NULL, route, "thread 1");
	pthread_create(&t2, NULL, route, "thread 2");
	pthread_create(&t3, NULL, route, "thread 3");
	pthread_create(&t4, NULL, route, "thread 4");
	//等待线程结束
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
}

发现多次执行这段代码得到的结果可能不同
为什么会出现不同的结果?


3. 多线程互斥详解

为啥上面可能会出现多种结果?
是有多种原因在里面的:

  1. if 语句判断条件为真以后,代码可以并发的切换到其他线程
  2. usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  3. - -ticket操作本身就不是一个原子操作

这里有一个问题,为什么- -ticket操作不是原子的?其实我们鉴定一个操作是不是原子性的可以查看这个操作的汇编代码,若汇编代码只有一条,则我们认为这个操作是原子的,反之则这个操作不是原子性的,可以来看看减减的汇编代码是有三条:

在这里插入图片描述
在这里插入图片描述

要解决上面的问题,需要满足以下条件:

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

本质上就是需要一把锁, 互斥锁

在这里插入图片描述

任何一个时间,只允许一个线程获得这把锁并且继续向后执行,没拿到锁的线程默认只能在加锁处阻塞等待其他线程释放掉锁才能继续往后走,多个线程来竞争一把锁,它们的关系就是互斥


4. 互斥锁的接口使用

互斥锁的使用一般分为四个步骤:

  1. 初始化互斥锁
  2. 在到达临界区前加锁
  3. 在跑完临界区代码后解锁
  4. 用完互斥锁后进行销毁

第一步: 初始化互斥锁

方法一, 静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二, 动态分配

在这里插入图片描述

第二步和第三步: 加解锁

在这里插入图片描述

调用pthread_ lock 时,可能会遇到以下情况:

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,
    那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

第四步: 销毁互斥锁

在这里插入图片描述

所以现在可以更改一下售票系统:

int ticket = 100;
pthread_mutex_t mutex;//全局
void *route(void *arg)
{
	char *id = (char*)arg;
	while ( 1 ) {
		pthread_mutex_lock(&mutex);
		if ( ticket > 0 ) {
			usleep(1000);
			printf("%s sells ticket:%d\n", id, ticket);
			ticket--;
			pthread_mutex_unlock(&mutex);
			// sched_yield(); 放弃CPU
		} else {
			pthread_mutex_unlock(&mutex);
			break;
		}
	}
}
int main( void )
{
	pthread_t t1, t2, t3, t4;
	pthread_mutex_init(&mutex, NULL);
	pthread_create(&t1, NULL, route, "thread 1");
	pthread_create(&t2, NULL, route, "thread 2");
	pthread_create(&t3, NULL, route, "thread 3");
	pthread_create(&t4, NULL, route, "thread 4");
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_join(t3, NULL);
	pthread_join(t4, NULL);
	pthread_mutex_destroy(&mutex);
}

5. 死锁相关概念

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。这样干说可能有点抽象,举个例子:

在这里插入图片描述

形成死锁的必要条件:

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

所以大家在写代码时,要避免写出死锁


6. 线程安全和可重入的关系

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

所以说,重入实际上比线程安全更加严格
下面是常见的不可重入的情况:

在这里插入图片描述

可重入和线程安全的联系和区别:

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

7. 线程同步基本概念

在多线程下并发的跑一段有加锁的代码,确实不会出现由于原子性而导致的不可预估的问题,但是也不代表这种方案就没有缺点了,想象一下以下的场景: 一共有6个线程在并发的执行一段代码,假如不做人为的干涉,当一个线程释放锁之后,下一个拿到锁的线程完全是不可预测的,也就是随机的,并且在加解锁这里,有一个可以称为就近原则的东西,就是说1号线程释放锁后,它此时距离锁最近,下一次获取锁可能还是1号线程,这就会导致其他线程虽然被创建出来了,但是并没有被调用,浪费的资源

不仅如此,当一个线程持有锁来到临界区后,可能临界区资源并没有就绪,比如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中.有没有一种方法可以让线程在资源不就绪时不要频繁的检测,一旦资源就绪就通知线程来访问资源?

要解决上面的问题,本质就是要线程同步
而线程同步坚持使用条件变量来实现

在这里插入图片描述


8. 线程同步的接口使用

使用条件变量通常分为四步:

  1. 初始化条件变量
  2. 利用条件变量等待资源就绪
  3. 资源就绪后唤醒线程来访问
  4. 使用完后销毁条件变量

第一步:

pthread_cond_t cond;//定义变量后再初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

在这里插入图片描述
第二步:

在这里插入图片描述

进行资源等待的前提是要拿到锁进入到临界区,所以wait函数既要你的条件变量,也要你的互斥锁,因为在后期资源就绪时,你这个线程是持有锁醒来的,继续该你执行

第三步:

在这里插入图片描述

broadcast函数是唤醒所有的线程,而signal是唤醒一个线程

第四步:
在这里插入图片描述
写一个简单的案例:

pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1( void *arg )
{
	while ( 1 ){
		pthread_cond_wait(&cond, &mutex);
		printf("线程被成功唤醒\n");
	}
}
void *r2(void *arg )
{
	while ( 1 ) {
		sleep(5);//等待一会儿再唤醒线程
		pthread_cond_signal(&cond);
		sleep(1);
	}
}
int main( void )
{
	pthread_t t1, t2;
	pthread_cond_init(&cond, NULL);
	pthread_mutex_init(&mutex, NULL);
	pthread_create(&t1, NULL, r1, NULL);
	pthread_create(&t2, NULL, r2, NULL);
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
}

9. 总结以及拓展

总的来说,多线程场景下还是比较容易出现各种错误的,所以在编写多线程的代码时,一定要对底层足够熟悉,对互斥锁以及条件变量要有一定的理解才能解决问题

一定要注意条件变量是在加解锁之间使用的


🔎 下期预告:生产者消费者模型 🔍

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

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

相关文章

比Let‘s Encrypt更简单更齐全的免费证书申请教程

步骤一 打开JoySSL官网&#xff0c;注册属于你的专属账号&#xff1b; 永久免费SSL证书申请地址真正完全且永久免费&#xff01;不用您花一分钱&#xff0c;SSL证书免费使用90天&#xff0c;并且还支持连续签发。JoySSL携手全球权威可信顶级根&#xff0c;自研新一代SSL证书&…

Elasticsearch:使用标记修剪提高文本扩展性能

作者&#xff1a;来自 Elastic Kathleen DeRusso 本博客讨论了 ELSER 性能的令人兴奋的新增强功能&#xff0c;该增强功能即将在 Elasticsearch 的下一版本中推出&#xff01; 标记&#xff08;token&#xff09;修剪背后的策略 我们已经详细讨论了 Elasticsearch 中的词汇和…

【Java 并发】AbstractQueuedSynchronizer 中的 Condition

1 简介 任何一个 Java 对象都天然继承于 Object 类, 在线程间实现通信的往往会应用到 Object 的几个方法, 比如 wait(), wait(long timeout), wait(long timeout, int nanos) 与 notify(), notifyAll() 几个方法实现等待 / 通知机制。同样的, 在 Java Lock 体系下也有同样的方…

【Python】进阶学习:计算一个人BMI(身体质量指数)指数

【Python】进阶学习&#xff1a;计算一个人BMI&#xff08;身体质量指数&#xff09;指数 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教…

考研失败, 学点Java打小工——Day3

1 编码规范——卫语句 表达异常分支时&#xff0c;少用if-else方式。   比如成绩判断中对于非法输入的处理&#xff1a; /*>90 <100 优秀>80 <90 良好>70 <80 一般>60 <70 及格<60 不及格*/Testpu…

蓝桥杯深度优先搜索|剪枝|N皇后问题|路径之谜(C++)

搜索&#xff1a;暴力法算法思想的具体实现 搜索&#xff1a;通用的方法&#xff0c;一个问题如果比较难&#xff0c;那么先尝试一下搜索&#xff0c;或许能启发出更好的算法 技巧&#xff1a;竞赛时遇到不会的难题&#xff0c;用搜索提交一下&#xff0c;说不定部分判题数据很…

李三清研究引领力学定律新篇章,光子模型图揭秘

一周期内&#xff0c;垂直&#xff0c;曲率不变&#xff0c;方向转向互变&#xff0c;正向反向互变&#xff0c;左旋右旋互变。变无限粗或变无限厚才发生质变&#xff0c;且属于由内向外变换&#xff0c;所以对应变换就是由内点向外点变换。 由于方向转向不能分割&#xff0c;…

画图实战-Python实现某产品全年销量数据多种样式可视化

画图实战-Python实现某产品全年销量数据多种样式可视化 学习心得Matplotlib说明什么是Matplotlib&#xff1f;Matplotlib特性Matplotlib安装 产品订单量-折线图某产品全年订单量数据数据提取和分析绘制折线图 产品订单&销售额-条形图某产品全年订单&销售额数据绘制条形…

Ollama管理本地开源大模型,用Open WebUI访问Ollama接口

现在开源大模型一个接一个的&#xff0c;而且各个都说自己的性能非常厉害&#xff0c;但是对于我们这些使用者&#xff0c;用起来就比较尴尬了。因为一个模型一个调用的方式&#xff0c;先得下载模型&#xff0c;下完模型&#xff0c;写加载代码&#xff0c;麻烦得很。 对于程…

windows中如何将已安装的node.js版本进行更换

第一步&#xff1a;先清除已经安装好的node.js版本 1.按健winR弹出窗口&#xff0c;键盘输入cmd,然后敲回车&#xff08;或者鼠标直接点击电脑桌面最左下角的win窗口图标弹出&#xff0c;输入cmd再点击回车键&#xff09; 然后进入命令控制行窗口&#xff0c;并输入where node…

upload文件上传漏洞复现

什么是文件上传漏洞&#xff1a; 文件上传漏洞是指由于程序员在对用户文件上传部分的控制不足或者处理缺陷&#xff0c;而导致的用户可以越过其本身权限向服务器上上传可执行的动态脚本文件。这里上传的文件可以是木马&#xff0c;病毒&#xff0c;恶意脚本或者WebShell等。“…

lua制作flash钢琴

效果预览 apk使用manaluax打包&#xff0c;源码在文末提供。 应用体验下载地址&#xff1a;https://www.magicalapk.com/appview?id1705213059764 源码 布局代码 {LinearLayout;gravity"center";layout_height"fill";orientation"vertical";…

蓝桥杯--冶炼金属

目录 一、题目 二、解决代码 &#xff08;1&#xff09;版本一&#xff08;报错&#xff1a;超时&#xff09; 代码分析 &#xff08;2&#xff09;版本二&#xff08;不会超时&#xff09; 代码分析 &#xff08;3&#xff09;版本三&#xff08;最终精简版&#xff09;…

【数据分析】数据分析介绍

专栏文章索引&#xff1a;【数据分析】专栏文章索引 目录 一、介绍 二、生活中的数据分析 1.无处不在的数据 2.为什么要进行数据分析&#xff1f; 三、数据挖掘案例 1.案例分析 一、介绍 数据采集&#xff1a;数据采集是指从不同来源收集原始数据的过程&#xff0c;包括…

孙宇晨最新研判:加密货币将成为全球金融基础设施的一部分

近日,波场TRON创始人、火币HTX全球顾问委员会委员孙宇晨接受了在加密社区有重要影响力的媒体平台Bankless的专访,就自己的从业经历、涉足加密行业的理想、波场TRON本身的发展和未来的市场走向等话题进行了详细的分享。 孙宇晨认为,波场TRON的使命是为那些没有银行账户的人提供…

Ubuntu——以桌面应用为主的Linux发行版操作系统

目录 一、Ubuntu简介 二、Ubuntu下载及安装 1.Swap分区的作用 2.语言环境 3.软件管理——apt 3.1配置文件 3.2软件源配置文件格式 3.3DPKG常用命令 三、常用命令总结 1. date——显示或设定系统的日期和与时间 2.cal——显示日历 3.设置时区 4.修改密码——passwd…

学习使用js获取当前ip地址的方法,使用第三方API获取ip地址

学习使用js获取当前ip地址的方法,使用第三方API获取ip地址 使用 DNS 查询使用第三方 API 使用 DNS 查询 DNS 是一种用于解析主机名为 IP 地址的系统。可以使用 JavaScript DNS 查询来获取本机IP地址。下面是如何使用 JavaScript 进行DNS查询的示例代码。 <p class"loc…

【数学】【计算几何】1453. 圆形靶内的最大飞镖数量

作者推荐 视频算法专题 本文涉及知识点 数学 计算几何 LeetCoce:1453. 圆形靶内的最大飞镖数量 Alice 向一面非常大的墙上掷出 n 支飞镖。给你一个数组 darts &#xff0c;其中 darts[i] [xi, yi] 表示 Alice 掷出的第 i 支飞镖落在墙上的位置。 Bob 知道墙上所有 n 支飞…

CASIA-HWDB手写体数据集gnt生成为png格式

👑一、数据集获取 1.1 官方链接获取gnt文件 http://www.nlpr.ia.ac.cn/databases/download/feature_data/HWDB1.1trn_gnt.ziphttp://www.nlpr.ia.ac.cn/databases/download/feature_data/HWDB1.1tst_gnt.zip 1.2 百度网盘获取gnt文件 链接:https://pan.baidu.com/s/1pKa…