Linux之线程及线程安全详解

前言:在操作系统中,进程是资源分配的基本单位,那么线程是什么呢?线程是调度的基本单位,我们该怎么理解呢?

目录

一,线程概念理解

二,Linux里面的线程原理

三,为什么要有线程

四,线程相关接口

1)线程创建

2)获取本线程ID

3)线程等待

4)线程取消

5)线程退出

五,多线程安全

1,互斥锁的原理

 2,互斥锁的使用

3,锁带来的饥饿问题

4,信号量

六,线程安全条件

1,常见的线程安全情况

2,常见的线程不安全情况

3,死锁


一,线程概念理解

现在我们举一个例子:我们以家庭为单位,将家庭看作一个整体,假如分配房子,车子等社会资源,这些资源被分配是以家庭为单位,这就类似于进程,每个进程执行一个大任务,进程之间的联系并不紧密并且相对独立。线程则类似与家庭里面的每一个人,家里面有许多为了完成大任务而拆分出来的小任务,比如孩子要上学,父母要上班,还有家务,家庭里面的每一个人有关联但也有各自不同的小任务,家庭里面的每一个人都共享所有资源,比如电视剧,客厅,车子——进程被分配的资源,但是同时它们也有自己的私人空间,线程同样如此,但线程的私人空间是

线程 ID
一组寄存器
errno
信号屏蔽字
调度优先级
为什么线程要有自己的私人空间呢?因为即使每个人都是为这个家庭(进程)做事,但是每个人做的事情不一样,我们需要一些私人数据才能完成不同的任务,而且为了区分不同的家庭成员(线程)我们也需要对他们进行起名(编号),这也是私人数据。

二,Linux里面的线程原理

大家有没有发现,线程和进程的功能其实有点类似,比如进程是执行一个复杂的大任务,而线程则是执行大任务里面拆分出来的小任务,并且它们都有自己的栈,共享进程的资源那我们可以使用进程的PCB来复用代替线程的TCB吗?答案是可以的,这样子不仅提高了代码的复用率,降低了编写的难度,让代码结构和维护变得更加简单,LinuxTCP的结构体就和PCB一样,但也可以自己编写一个独立的TCB,比如:windows系统。但也有所不同,进程号是标识进程唯一性的编号,而线程号则是一个地址——线程地址。
Linux里面的线程被称作轻量级进程,我们需要理清楚线程(轻量级进程)和进程之间的关系,进程是一组线程集合,一个进程最少有一个线程,线程则是进程里面的一个执行流(执行小任务)

三,为什么要有线程

大家可能很好奇为什么有进程了还要有线程,一个CPU一个时间段执行一个指令吗?线程虽称并行执行流,但底层还是不能同时执行,也得排队。

现在举一个例子:有一个进程A需要执行一个任务,从外设输入字符串并且打印到屏幕上,并且要计算一些加减乘除。

如果只有一个进程我们只能顺序执行,也就是当输入输出的时候我们不能干其他事必须等待,而IO流的速度是很慢的,如果我们一直等待不就把CPU的资源浪费了吗?如果我们利用线程呢?一个线程负责IO流,一个线程负责计算,这样子当我们进行IO操作的时候,我们可以把它挂起,利用CPU去进行计算,当IO操作完成再唤醒执行这个线程,这样子CPU的利用率就提高了,执行效率也提高了。

但是有人可能有疑问,我们为什么不切换到下一个进程,等到IO操作完成再唤醒这个进程呢?这就涉及到了开销的问题,进程之间是相互独立的,我们切换是需要进行保存现场等工作,这样子不断的切换开销是很大的,而线程它们之间的切换的开销小的很多,他们的页表,数据都是共享的。

那么线程适合什么样的场景使用,线程是越多越好吗?
线程适合IO操作较多的场景,计算流操作效果比较差,因为CPU的算力是有限的,同一时间只允许一个计算任务。

线程并不是越多越好,因为线程也是有开销的,例如TCB结构体,栈,对这些进行管理也要开销。

四,线程相关接口

在Linux里面实际上是没用线程的,只有轻量级进程,但是为了迎合主流,Linux还是对轻量级进程进行了包装成了线程库,因此在编译时要连接原生线程库,在g++指令后面加上  -lpthread,例如:

g++ -o test.o test.cpp -std=c++11 -lpthread
线程接口使用类似于进程间通信的接口,首先我们要认识一下区分线程的唯一标识符,也就是线程地址
pthread_t 

底层实际上就是一个unsigned int 

1)线程创建

 第一个参数thread会通过指针返回创建线程的ID,第二个参数大部分情况是NULL,用来调整线程的属性,第三个参数是线程执行的函数,第四个参数是执行函数的参数。成功返回0,失败返回错误编号。

void* task(void* arg){
int* i=(int*)arg;
cout<<"这是一个线程任务:"<<*i;
}
pthread_t tid;
int i=1;
pthread_create(&tid,NULL,task,(void*)i);
2)获取本线程ID

没有参数,返回值就是本线程的ID。

3)线程等待
第一个参数是等待线程ID,第二个是对线程进行管理的参数,一般默认NULL。
这个函数的作用是等待一个线程结束,成功返回0,失败返回错误编号,在这个线程结束前这个函数不会结束。
pthread_t tid=pthread_self();
pthread_join(tid,NULL);

4)线程取消
函数参数是取消线程的ID,操作成功返回0。
pthread_t tid=pthread_self();
pthread_cancel(tid);
5)线程退出

这个作用于pthread_exit作用效果类似,但是只能退出本线程,不能退出指定线程。

五,多线程安全

大家有没有想过多线程执行会不会带来安全问题,答案是必然的,为什么呢?因为线程简单切换会发生在如何不是原子代码执行的时候(原子性是指代码执行不会被中断,要么不开始,一开始就必须执行完,不存在中间状态),

int tictik=100;
void* RobTictik(void* arg){
while(titck>0){
cout<<"线程:"<<pthread_self()<<"抢到了票"<<tictik--<<endl;
}
}

如果所有线程执行这个函数,很有可能就会出现tictik最后小于0的情况,也就是最后卖出了多于100张的票,为什么呢?假如tictik已经是1了,线程A刚刚进入还未来得及打印将tictik打印就切换到了线程B,就会出现多卖票的情况。

那我们有什么办法解决吗?

1,互斥锁的原理

互斥锁是什么呢?人如其名它的功能类似于一把锁,你进去时候加上一把锁,当别人试图进来的时候就会因为没有钥匙而无法进来,你出去的是就把锁换回去,让其他想进来的人竞争这把钥匙。

锁的原理是什么呢?其实挺简单的,就是锁里面本来有一个1,当线程切换的时候线程会把自己的上下文保存,将数据1拿走,其他线程走到这块区域的时候就发现是0无法运行,继续等待抢锁,直到线程将这块区域运行完才会将锁换回去。下面这个图就是类似我讲述的锁原理。

 2,互斥锁的使用

互斥锁的使用需要初始化,然后加锁,解锁。

初始化有两种方式,一种是全局锁,一种是局部锁(作用域)

这是互斥锁的结构体

全局互斥锁初始化

局部互斥锁的初始化 

第一个参数是锁结构体,第二个参数一般填NULL。

加锁

成功返回0,需要注意的是加锁代码是原子性的,防止多个线程进入锁

解锁

成功返回0,注意解锁并不是原子性的,因为解锁时是不是原子性已经不重要了,如果锁已经归还,多线程也只能有一个抢到,如果还未归还不过是让其他线程多等等。

pthread_mutex_init(&_mutex,NULL);
 pthread_mutex_lock(&p->_mutex);
           //临界区代码,被保护,原子性 
 pthread_mutex_unlock(&p->_mutex);

3,锁带来的饥饿问题

互斥锁的抢夺是公平的,但是有一些线程的抢锁能力强,这就会导致一个问题,一个线程长期霸占着锁,其他线程就一直无法运行代码,导致饥饿问题,那有什么解决办法吗?答案是条件变量。

条件变量是什么呢?之前我们举例子所有人抢钥匙开门,现在我们加一个规矩,那就是排队,新来的和出去的只能从后面开始排队,而且这段时间你们都处于休眠,直到轮到你们有人唤醒你们才继续执行。

条件变量使用很类似于互斥锁

条件变量结构体

初始也分全局初始化和局部初始化

全局条件变量初始化

局部条件变量初始化

第一个参数是条件变量结构体,第二个参数一般是NULL。

互斥锁的使用一般是放在互斥锁里面的,如果将线程放入条件队列,会先解锁,然后继续抢锁,因此建议进入互斥锁临界区就先检查是否需要放入条件队列等待

参数一是条件变量结构体,参数二是互斥锁,因为条件变量是需要结合互斥锁使用的。

条件变量的唤醒,我们直到进入条件变量等待队列后是无法自己醒来的,需要使用函数唤醒

唤醒指定条件变量里面的一个线程,成功返回0

唤醒指定条件变量里面的所有线程,成功返回0

破坏条件变量

pthread_mutex_init(&_mutex,NULL);
pthread_cond_init(&cond,NULL);
 pthread_mutex_lock(&p->_mutex);
while(条件不满足){
 pthread_cond_wait(&cond,&mutex);
}
           //临界区代码,被保护,原子性 
 pthread_mutex_unlock(&p->_mutex);

上面的代码为什么要用循环来判断条件是否满足呢?因为即使抢到锁了条件也不一定满足,如果是if语句就会直接执行接下里的代码,导致线程安全问题 

4,信号量

在Linux里面信号量也是保护线程安全的一种重要手段,一般也是结合互斥锁使用

信号量的原理就是计数器,但是对计数器的操作是原子性的,举个例子,假如盆里面有十个苹果,有三个人都想抢苹果,三个让可以同时拿苹果,但是不能抢同一个苹果,信号量就是保护你们不抢同一个苹果。

信号量结构体

信号量的初始化只有一种

第二个参数一般设置为0,第三个参数是sem量的初始值,类似于上面的盆子里有几个苹果,成功返回0。

申请信号量,也就是上面申请抢一个苹果,成功返回0.

释放信号量,相当于有人往盆里放苹果,成功返回0。

六,线程安全条件

什么样的线程有风险,什么样的线程是安全的呢?

1,常见的线程安全情况

只读不写

执行流里面的写操作都是原子性的

多个线程切换不存在二义性

2,常见的线程不安全情况

不保护多线程共享的变量

执行流的状态随着执行,被调用状态发生变化

返回指向静态变量的函数

调用线程不安全的函数

3,死锁

死锁是指各自不释放自己占有有资源,但因为有资源抢夺不到而都无法导致一种尴尬的场景。举个例子,想要打开一个宝箱需要两个要是,有两个人各自持有一把锁(线程各自持有一个锁),双方互不相让,导致谁也打不开宝箱,死锁和多个锁之间分配顺序的不同有很大关系。

死锁但是有四个必要的条件

1,不可剥夺性,线程占有资源互不相让,别人无法强行抢夺自己以有的资源

2,互斥条件 ,一个资源不能同时被多个人使用

3,请求和保持条件,一个执行流因请求资源而阻塞时,对已获得的资源保持不放

4,循环等待条件,形成了环路,造成了尴尬的场面,谁也无法好过。

如何避免死锁

破坏上面的四个形成的必要条件之一,死锁就不攻自破

加锁顺序一致,防止各自持有对方所需的资源

避免锁未释放,资源被锁死

资源一次性释放

银行家算法:模拟资源分配,如果产生了死锁就撤销任务不分配资源

死锁检测算法

拓展:C++里面的各种STL容器为了追求效率是没用加锁的,使用的时候要注意线程安全。

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

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

相关文章

《MySQL索引》学习笔记

《MySQL索引》学习笔记 MySQL的体系结构存储引擎简介InnoDB简介MyISAM简介 索引索引结构BTreeHash索引思考索引分类 索引语法SQL性能分析索引使用最左前缀法则 索引失效的情况范围查询索引列运算字符串不加引号模糊查询or连接的条件数据分布影响 SQL提示覆盖索引前缀索引单列索…

【MyBatisPlus】DML编程控制

【MyBatisPlus】DML编程控制 文章目录 【MyBatisPlus】DML编程控制1、id生成策略2、逻辑删除 1、id生成策略 id生成策略控制&#xff08;TableId注解&#xff09; 名称&#xff1a;TableId 类型&#xff1a;属性注解 位置&#xff1a;模型类中用于表示主键的属性定义上方 作…

机器学习中的集成学习

&#x1f4ac;内容概要 1 集成学习概述及主要研究领域 2 简单集成技术  2.1 投票法  2.2 平均法  2.3 加权平均 3 高级集成技术  3.1 Bagging  3.2 Boosting  3.3 Bagging vs Boosting 4 基于Bagging和Boosting的机器学习算法  4.1 sklearn中的Bagging算法  4.2 sklea…

AI大模型探索之路-实战篇15: Agent智能数据分析平台之整合封装Tools和Memory功能代码

系列篇章&#x1f4a5; AI大模型探索之路-实战篇4&#xff1a;深入DB-GPT数据应用开发框架调研 AI大模型探索之路-实战篇5&#xff1a;探索Open Interpreter开放代码解释器调研 AI大模型探索之路-实战篇6&#xff1a;掌握Function Calling的详细流程 AI大模型探索之路-实战篇7…

C++基础-vector容器

目录 零. 前言: 一.简介 二. 主要特点 三. 例子 1.创建 2.添加元素 3.访问元素 4.获取大小 5.删除元素 6.扩展 begin() end() 零. 前言: 在编程中&#xff0c;数组通常具有固定的大小&#xff0c;这在某些情况下可能会带来一些限制。 当我们事先无法确切知道需要存…

topK 问题

topK 问题 topK二、实验内容三、数据结构设计四、算法设计五、运行结果六、程序源码 topK &#xff08;1&#xff09;实验题目 topK 问题 &#xff08;2&#xff09;问题描述 从大批量数据序列中寻找最大的前 k 个数据&#xff0c;比如从 10 万个数据中&#xff0c;寻找最大的…

leetcode155 最小栈

题目 设计一个支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常数时间内检索到最小元素的栈。 实现 MinStack 类: MinStack() 初始化堆栈对象。void push(int val) 将元素val推入堆栈。void pop() 删除堆栈顶部的元素。int top() 获取堆栈顶部的元素。i…

OpenCv之简单的人脸识别项目(特征标注页面)

人脸识别 准备八、特征标注页面1.导入所需的包2.设置窗口2.1定义窗口外观和大小2.2设置窗口背景2.2.1设置背景图片2.2.2创建label控件 3.定义两个全局变量4.定义选择图片的函数4.1函数定义和全局变量声明4.2打开文件对话框并获取文件路径4.3处理图片并创建标签4.4显示图像 5.定…

Window11端口开放防火墙

&#xff08;1&#xff09;打开控制面板&#xff0c;进入【控制面板\系统和安全\Windows Defender 防火墙】 &#xff08;2&#xff09;点击左侧菜单【高级设置】&#xff0c;进入防火墙设置页面 &#xff08;3&#xff09;根据需要选择【入站规则】或者【出站规则】&#xff…

【深度好文】到底什么是质量意识?如何衡量,如何提升?

大家好&#xff0c;我是狂师&#xff01; 在软件测试中&#xff0c;质量意识是一个核心且至关重要的概念。相信大家&#xff0c;经常会听到&#xff1a;"这个家伙质量意识很强&#xff0c;某某某要提升质量意识“之类的话语。 在企业中&#xff0c;“质量意识”不仅关乎…

NoSQL实战(MongoDB搭建主从复制)

什么是复制集&#xff1f; MongoDB复制是将数据同步到多个服务器的过程&#xff1b; 复制集提供了数据的冗余备份并提高了数据的可用性&#xff0c;通常可以保证数据的安全性&#xff1b; 复制集还允许您从硬件故障和服务中断中恢复数据。 保障数据的安全性 数据高可用性 (2…

day30--mybatis(三)高级

一.Mybatis注解开发单表操作 1.1 MyBatis的常用注解 这几年来注解开发越来越流行&#xff0c;Mybatis也可以使用注解开发方式&#xff0c;这样我们就可以减少编写Mapper 映射文件了。我们先围绕一些基本的CRUD来学习&#xff0c;再学习复杂映射多表操作。 Insert&#xff1…

【数据结构】从前序与中序遍历,或中序与后序遍历序列,构造二叉树

欢迎浏览高耳机的博客 希望我们彼此都有更好的收获 感谢三连支持&#xff01; 首先&#xff0c;根据先序遍历可以确定根节点E&#xff0c;再在中序遍历中通过E确定左树和右数 &#xff1b; 设立inBegin和inEnd&#xff0c;通过这两个参数的游走&#xff0c;来进行子树的创建&a…

springboot配置集成RedisTemplate和Redisson,使用分布式锁案例

文章要点 自定义配置属性类集成配置RedisTemplate集成配置分布式锁Redisson使用分布式锁简单实现超卖方案 1. 项目结构 2. 集成RedisTemplate和Redisson 添加依赖 依赖的版本与继承的spring-boot-starter-parent工程相对应&#xff0c;可写可不写 <!--spring data redis…

SylixOS网卡多 IP 配置

概述 网卡多 IP 是指在同一个网络接口上配置和绑定多个 IP 地址。 引进网卡多 IP 的目的主要有以下几个&#xff1a; 提供服务高可用性。通过在同一接口绑定多个 IP 地址&#xff0c;然后在服务端使用这些 IP 地址启动多个服务实例。这样在任意一 IP 出现问题时&#xff0c;可…

Ollama教程——使用Ollama与LangChain实现Function Calling(函数调用)的详细教程(一)

@[toc](Ollama教程——使用Ollama与LangChain实现Function Calling(函数调用)的详细教程(一)) 在本教程中,我们将介绍如何使用Ollama和LangChain实现函数调用任务。这种方法可以大大提高AI模型在特定任务上的性能。本文将详细解释如何设置、使用OllamaFunctions,并通过多个…

openEuler Embedded 系统 实时性

openEuler Embedded 系统 & 实时性 1 介绍1.1 概述1.2 openEuler 23.09 Embedded1.3 openEuler 重要节点1.4 系统构建工具1.5 openEuler Embedded 诞生的需求背景运动控制系统实时性需求高嵌入式OS主要供应商来自老美&#xff0c;市场碎片化严重 1.6 总体架构1.7 openEuler…

AI预测体彩排3采取888=3策略+和值012路一缩定乾坤测试6月3日预测第10弹

昨天的第二套方案已命中&#xff01;今天继续基于8883的大底进行测试&#xff0c;今天继续测试&#xff0c;好了&#xff0c;直接上结果吧~ 首先&#xff0c;888定位如下&#xff1a; 百位&#xff1a;6,4,7,8,2,9,1,0 十位&#xff1a;2,3,4,1,6,7,8,…

000002 - Hadoop环境安装

Hadoop及其大数据生态圈 1. 背景2. 实践2.1 Linux服务器准备2.2 在其中一台服务器上安装JDK2.3 在其中一台服务器上安装HADOOP2.4 本地模式运行一个hadoop案例 3. 自动化部署 1. 背景 要搭建Hadoop集群环境&#xff0c;我们需要执行如下 准备三台Linux服务器&#xff0c;服务…

基于三元组一致性学习的单目内窥镜里程计估计

文章目录 TCL: Triplet Consistent Learning for Odometry Estimation of Monocular Endoscope摘要方法实验结果 TCL: Triplet Consistent Learning for Odometry Estimation of Monocular Endoscope 摘要 单目图像中深度和姿态的估计对于计算机辅助导航至关重要。由于很难获…