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

news2024/11/16 7:46:37

💓博主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. 总结以及拓展

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

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


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

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

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

相关文章

pip 配置镜像加速安装

在使用pip安装Python第三方库时&#xff0c;默认是使用pip官网的非常慢&#xff0c;可通过配置国内镜像源加速下载速度&#xff0c;以下是如何使用国内镜像源安装Python库的两种常见方式&#xff1a; 临时使用镜像源安装 如果你只是想临时使用某个镜像源安装单个或几个库&…

让el-input与其他组件能够显示在同一行

让el-input与其他组件能够显示在同一行 说明&#xff1a;由于el-input标签使用会默认占满一行&#xff0c;所以在某些需要多个展示一行的时候不适用&#xff0c;因此需要能够跟其他组件显示在同一行。 效果&#xff1a; 1、el-input标签内使用css属性inline 111<el-inp…

Mac-自动操作 实现双击即可执行shell脚本

背景 在Mac上运行shell脚本&#xff0c;总是需要开启终端窗口执行&#xff0c;比较麻烦 方案 使用Mac上自带的“自动操作”程序&#xff0c;将shell脚本打包成可运行程序(.app后缀)&#xff0c;实现双击打开即可执行shell脚本 实现细节 找到Mac上 应用程序中的 自动操作&am…

1 redis7概述

Redis7 1 Redis简介 Redis之所以称之为字典服务&#xff0c; 是因为 Redis 是一个 key-value存储系统。 支持存储的 value类型很多&#xff0c; 包括 String(字符串)、List(链表)、Set(集合)、Zset(sorted set --有序集合)和 Hash&#xff08;哈希类型&#xff09;等。 Redis…

Flink通讯模型—Akka与Actor模型

Carl Hewitt 在1973年对Actor模型进行了如下定义&#xff1a;"Actor模型是一个把Actor作为并发计算的通用原语". Actor是异步驱动&#xff0c;可以并行和分布式部署及运行的最小颗粒。也就是说&#xff0c;它可以被分配&#xff0c;分布&#xff0c;调度到不同的CPU&…

2024.3.14 C++

思维导图 封装类 用其成员函数实现&#xff08;对该类的&#xff09;数学运算符的重载&#xff08;加法&#xff09;&#xff0c;并封装一个全局函数实现&#xff08;对该类的&#xff09;数学运算符的重载&#xff08;减法&#xff09;。 #include <iostream>using nam…

202012青少年软件编程(图形化) 等级考试试卷(二级)

青少年软件编程(图形化) 等级考试试卷(二级)2020年12月 第1题:【 单选题】 实现角色一直移动, 碰到舞台边缘发出声音, 正确的选项是?( ) A: B: C: D: 【正确答案】: B 【试题解析】 : 第2题:【 单选题】 执行下图所示的程序, 得到的结果是?( ) A:…

c++ 面试100个题目中的编程题目

88、下列程序的运行结果是? #include <stdlib.h> #include <stdio.h> #include <string.h> #include <iostream> const char* str = "vermeer"; using namespace std; int main(){ const char* pstr = str;cout << "The add…

SAT和SMT介绍及求解器使用

一、SAT 1、介绍 &#xff08;1&#xff09;定义 SAT即命题逻辑公式的可满足性问题/布尔可满足性问题。即给定一个与或非和变量组成的命题公式&#xff0c;判断是否存在一些结果使得这个公式成立 它是第一个被确认为NP完全的问题。 输入&#xff1a;析取范式&#xff08;C…

Linux 大页内存 Huge Pages 虚拟内存

Linux 大页内存 Huge Pages 虚拟内存 - 秋来叶黄 - 博客园 (cnblogs.com) Linux为什么要有大页内存&#xff1f;为什么DPDK必须要设置大页内存&#xff1f;这都是由系统架构决定的。一开始为了解决一个问题&#xff0c;设计了对应的方案&#xff0c;随着事物的发展&#xff0c…

【CesiumJS-5】绘制动态路线实现飞行航线、汽车轨迹、路径漫游等

实现效果 前言 Cesium中&#xff0c;动态路线绘制的核心是借助CZML格式&#xff0c;CZML是一种用来描述动态场景的JSON数组,可以用来描述点、线、多边形、体、模型及其他图元,同时定义它们是怎样随时间变化的&#xff1b; CZML主要做三件事&#xff1a; 1.添加模型信息 2.添加…

Hadoop大数据应用:Yarn 节点实现扩容与缩容

目录 一、实验 1.环境 2.Yarn 节点扩容 3.Yarn 节点缩容 二、问题 1.yarn启动服务报错 一、实验 1.环境 &#xff08;1&#xff09;主机 表1 主机 主机架构软件版本IP备注hadoop NameNode &#xff08;已部署&#xff09; SecondaryNameNode &#xff08;已部署&…

【vue baidu-map】marker鼠标悬浮点击事件失效

要实现的效果&#xff1a;鼠标悬浮或者点击标注点会出现弹窗 验证过鼠标点击悬浮代码没问题&#xff0c;最后发现是控件样式影响的 ::v-deep #bjmap .BMap_noprint {inset: 10px 90% auto auto !important; } 只要增加上述样式&#xff0c;鼠标悬浮事件就会失效

QT使用dumpcpp为COM生成h及cpp的方式,COM是C#的dll注册的

目录 1.C#的dll注册为COM&#xff0c;采用bat的方式 2.通过qt的dumpcpp来生成h及cpp文件 3.h文件和cpp文件处理。 台达数控系统的C#的dll dumpcpp用的tlb文件 dumpcpp生成的原生h文件 dumpcpp生成的原生cpp dump生成后的的原生cpp文件修改后的cpp文资源 dump生成后的的…

【探索Linux】—— 强大的命令行工具 P.27(网络编程套接字 —— UDP协议介绍 | TCP协议介绍 | UDP 和 TCP 的异同)

阅读导航 引言一、UDP协议1. UDP简介2. UDP的特点3. UDP的使用场景4. UDP的局限性 二、TCP协议1. TCP简介2. TCP的特点3. TCP的应用场景 三、UDP 和 TCP 的异同温馨提示 引言 在上一篇文章中&#xff0c;我们深入探讨了Linux网络编程的基石——套接字&#xff08;Socket&#…

pytorch loss及其梯度

目录 1.loss的种类1.1 MSE1.2 MSE推导1.3 autograd.grad1.4 loss.backward 2. Softmax2.1 softmax推导 1.loss的种类 常见两种一种是均方差&#xff0c;一种是交叉熵 1.1 MSE 1.2 MSE推导 1.3 autograd.grad 1.4 loss.backward 注意&#xff1a;autograd.grad直接返回梯度&a…

守护健康,从营养开始 —— 帕金森患者的饮食秘籍

亲爱的读者朋友们&#xff0c;您是否知道&#xff0c;在对抗帕金森病的道路上&#xff0c;正确的饮食和营养补充可以成为我们的有力盟友&#xff1f;今天&#xff0c;就让我们一起探索那些能够帮助帕金森患者改善症状、提高生活质量的营养素&#xff0c;开启健康生活的新篇章。…

【保姆级】GPT的Oops问题快速解决方案

GPT的"Oops"问题通常指的是GPT在处理请求时突然遇到错误或无法提供预期输出的情况。要快速解决这个问题&#xff0c;可以尝试以下分步策略&#xff1a; 确认问题范围&#xff1a; 首先&#xff0c;确认问题是偶发的还是持续存在的。如果是偶发的&#xff0c;可能是临…

深度学习_GoogLeNet_4

目标 知道GoogLeNet网络结构的特点能够利用GoogLeNet完成图像分类 一、开发背景 GoogLeNet在2014年由Google团队提出&#xff0c; 斩获当年ImageNet(ILSVRC14)竞赛中Classification Task (分类任务) 第一名&#xff0c;VGG获得了第二名&#xff0c;为了向“LeNet”致敬&#x…

不同抓手的码垛机:适配多元应用场景的灵活之选

在现代工业生产中&#xff0c;码垛机作为一种高效、自动化的物料搬运设备&#xff0c;已经广泛应用于各个行业。而抓手作为码垛机的关键部件&#xff0c;其种类的多样性和适用场景的广泛性&#xff0c;使得不同抓手的码垛机能够满足各种复杂、多变的生产需求。 首先&#xff0c…