【linux】多线程概念详述
文章目录
- 一、线程基本概念
- 1.1 进程地址空间与页表
- 1.2 页表结构
- 1.3 线程的理解
- 1.3.1 如何描述线程
 
- 1.4 再谈进程
- 1.5 代码理解
- 1.5.1 原生库提供线程pthread_create
 
- 1.6 资源共享问题
- 1.7 资源私有问题
 
- 二、总结
- 2.1 什么是线程
- 2.2 并行与并发
- 2.3 线程的优点
- 2.4 线程的缺点
- 2.5 线程异常
- 2.6 进程与线程间的关系
 
一、线程基本概念
1.1 进程地址空间与页表

注意这里的页表部分:
在上一章【linux】进程信号——信号的保存和处理中我们讲了页表有用户级页表和内核级页表。如图其实页表还有其他很多属性。
举个例子:当我们对常量区的数据进行修改时,为什么会报错呢?
OS会先通过页表找到物理地址,然后查RWX权限,发现只有R权限,所以地址转换单元MMU会硬件报错,转化成信号,终止进程(段错误)。
而经过U/K权限的时候,如果是U就直接访问,是K就会去CPU查看当前的运行级别,是内核级别才能访问带K的映射关系。
那么该如何看待进程地址空间和页表呢?
1️⃣ 进程地址空间(虚拟内存)是进程能够看到的资源窗口。因为能看到的资源都是通过进程地址空间让我们看到的。
2️⃣ 页表是决定进程真正拥有资源的情况。
3️⃣ 合理的对进程地址空间+页表进行资源划分,就可以对进程的所有资源进行分类。
进程地址空间一共有2^32个地址。那么按道理页表也应该有2^32个条目,我们就当一个条目大小为1byte,也需要4GB的大小,更何况每个条目还得存很多数据。所以页表不可能会这么使用。
 那么真实的页表到底是什么样子呢?
1.2 页表结构
首先说明几个点:
1️⃣ 进程地址空间的一个地址我们称为虚拟地址,有32个比特位。
2️⃣ 物理内存实际上也划分成了一个一个的数据页。OS为了管理每个数据页,每个数据页都有一个描述的结构体(非常小),存储内存的属性。每个页框大小为4KB
3️⃣ 磁盘上的可执行程序在被编译的时候也被划分成一个一个的4KB大小的数据块,我们把这种4KB的区域称为页帧。所以从磁盘加载到内存是以4KB为单位加载的。
虚拟地址的32个比特位并不是以一个整体转化的,而是分成10、10、12三块二进制构成。
 而页表也不止一张,分为页目录、和页表。
 先拿着虚拟地址的高十位去查页目录,比如如果是0000000001就是第二个位置,映射到指定的页表,再通过中间10个比特位确定物理内存中页框的起始地址,而一个页框的大小是4KB,有2^12字节,刚好对应虚拟地址的低12位比特位,就可以作为页内偏移量找到对应的位置。
 
 这样我们在使用的时候有可能只使用了几个页表,那么其他的页表就不会加载到内存,只有需要的时候才会创建。由此解决了内存不足的问题。
1.3 线程的理解
首先要知道线程是进程内的一个执行流。
我们知道创建一个进程就会连着创建PCB,虚拟内存、页表。现在我们可以创建一个“进程”(PCB)直接指向虚拟内存,就像下边的绿色的task_struct。
 
 例如代码区有一大段代码,我们现在就可以划分成几个小段代码,分给每个“进程”。这样就实现了资源的分配。
我们可以通过虚拟地址空间和页表对进程进行资源划分,而单个“进程”的执行力度一定要比之前一个进程要细。
1.3.1 如何描述线程
既然有多个线程,那么OS就会采取先描述后组织的方式进行管理。那么怎么描述呢?是创建一个新的结构体来描述吗?
我们知道PCB是用来描述进程的,那么描述线程的结构体我们叫做TCB(线程控制块)。
在windows中,就是新创建了一个结构体来描述线程。
而单纯的从线程调度角度,进程和线程有很多地方是重叠的。
所以在linux中,没有创建针对线程的数据结构,而是直接复用PCB,用PCB来表示线程。
而CPU在进行调度的时候不关注到底是进程还是线程,只看task_struct。
总结一下:线程在进程内部(进程的地址空间内)执行,拥有该进程的一部分资源。
1.4 再谈进程
什么叫做进程呢?
 
 我们把红色框框圈起来的整体叫做进程:
 PCB+进程地址空间+页表+加载到物理内存的代码和数据。
从内核角度:进程是承担分配系统资源的基本实体
 在linux中:线程是CPU调度的基本单位
而在之前的文章讲过的进程【linux】进程概念详述它讲的是只有一个PCB的进程(只有一个执行流)。
 今天所讲述的是一个进程内有多个执行流的情况。
从CPU角度:以前调度的就是一个进程,今天就是调度进程中的一个分支。
 所以现在CPU统一把task_struct看作成轻量级进程。
我们知道linux没有正真意义的线程,这相比拥有真正线程的系统有什么优缺点呢?
优点:简单,维护成本大大降低,即可靠又高效。
缺点:linux无法直接提供线程的基本调用接口,只能提供创建轻量级进程的接口。
1.5 代码理解
1.5.1 原生库提供线程pthread_create
#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.// 链接的时候必须加上-lpthreadRETURN VALUE
On success, pthread_create() returns 0; 
on error, it returns an error number, and the contents of *thread are undefined.
参数说明:
 thread:线程id
 attr:线程属性,直接设为null
 start_routine:函数指针
 arg:这个参数会传递进start_routine的void*参数中。
这里在链接的时候要注意link到系统给的原生线程库-lpthread。

 说明一下这个原生线程库:
 因为用户只关注线程,但是OS不提供线程的接口,只提供创建轻量级进程的接口。所以在用户和OS之间加了一个用户级线程库。 向上提供各种线程接口,向下把对线程的各种操作转化为对轻量级进程的各种操作。
 这个库在任何linux操作系统都默认存在。
// Makefile
mythread:mythread.ccg++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:rm -f mythread// mythread.cc
#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>using std::cout;
using std::endl;void* thread_stream(void *str)
{while(true){cout << "i am new thread" << endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one");assert(n == 0);(void)n;// 主线程while(true){cout << "i am main thread" << endl;sleep(1);}return 0;
}

 现象:运行了两个执行流,查看只有一个进程,杀死进程两个执行流全部被杀死。
 如果想看到这两个轻量级线程:
 使用指令:ps -aL
 
 可以看到两个PID一样,说明属于同一个进程。而这里可以看到LWP不同,这里的LWP就表示轻量级进程ID。
 细节:主线程的PID和LWP一样。
所以CPU在调度的时候用的就是LWP来作为标识符表示特定的执行流。
 当只有一个单进程的时候PID和LWP是等价的。
那么这个tid到底是什么呢?
 我们可以修改一下代码进行验证:
int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");assert(n == 0);(void)n;// 主线程while(true){char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);cout << "i am main thread " << "tid: " << buf << endl;sleep(1);}return 0;
}

 这里只需要知道tid就是一个地址,后面会详细介绍。
1.6 资源共享问题
线程一旦被创建,几乎所有的资源都是被所有线程共享的。
 比如:
文件描述符表
每种信号处理方式(SIG,IGN,SIG_DFL或者自定义信号处理函数)
当前工作目录
用户id和组id
#include <iostream>
#include <pthread.h>
#include <cassert>
#include <unistd.h>
#include <cstdio>using std::cout;
using std::endl;void fun()
{cout << "这是一个独立的方法" << endl;
}void* thread_stream(void *str)
{while(true){cout << "i am new thread, name: " << (const char*)str;fun();sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");assert(n == 0);(void)n;// 主线程while(true){char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);cout << "i am main thread " << "tid: " << buf;fun();sleep(1);}return 0;
}

 可以看到这个函数可以被多个线程同时访问。
那么全局变量呢?
int cnt = 0;void* thread_stream(void *str)
{while(true){cout << "i am new thread, name: " << (const char*)str << " cnt: " << cnt++ << " &cnt: " << &cnt << endl;sleep(1);}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");assert(n == 0);(void)n;// 主线程while(true){char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);cout << "i am main thread " << "tid: " << buf << " cnt: " << cnt << " &cnt: " << &cnt << endl;sleep(1);}return 0;
}

 只要有一个线程中改变了,也会影响另一个进程。
 由此可见线程之间通信非常容易。
但是这样又会引发另一个问题。
1.7 资源私有问题
线程也要有自己的私有资源,那么什么资源应该是线程所私有的呢?
1️⃣ PCB的属性(优先级,上下文(线程动态切换),状态……)。
2️⃣ 每一个线程都有自己独立的栈结构保存私有数据。
二、总结
2.1 什么是线程
笼统的讲:线程是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细致和轻量化。
- 在一个程序里的一个执行路线叫做线程,更准确的定义是:线程是”一个进程内部的控制序列“
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼里,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
2.2 并行与并发
并行:多个执行流在同一刻拿着不同的CPU进行运算。
 并发:多个执行流在同一时刻只有一个执行流拥有CPU进行运算。
2.3 线程的优点
1️⃣ 创建线程的代价比创建进程小得多。因为不用创建地址空间、页表、加载代码数据,只用创建一个PCB指向进程地址空间就够了。
 2️⃣ 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
 进程要切换 页表、PCB、虚拟地址空间……
 而线程切换只用切换PCB
 那么他们做的工作量到底差距在哪里呢?
在CPU中有一块高速缓存cache,它的效率比寄存器慢,但比内存快。它有局部性原理:当前访问代码附近的代码数据也会被加载进来,有较大的概率被访问到。CPU不会从内存中直接读取数据,而是从cache中获取,没有命中就再从内存中加载数据到cache。而一个已经运行一段时间的进程cache内部会有很多“热点数据”,线程切换的时候并不会更新chche的数据(因为这些热点数据本来就是被线程所共享的),但是进程切换的时候chache内的数据立刻更新。这样chache又得重新缓存数据。
3️⃣ 线程的占有资源比进程小得多。
 4️⃣ 能充分利用多处理器的可并行数量。
 5️⃣ 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
 6️⃣ 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。计算密集型应用最常见的情况有:加密,大数据运算等—主要使用的是CPU资源。
 7️⃣ I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
2.4 线程的缺点
1️⃣ 性能损失
 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
 2️⃣ 健壮性(鲁棒性)降低
 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
 验证:一个线程出现异常会影响其他线程吗?
void* thread_stream(void *str)
{while(true){cout << "i am new thread, name: " << (const char*)str << endl;sleep(1);// 一个线程出现异常int* p = nullptr;*p = 100;}
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, thread_stream, (void*)"thread one ");assert(n == 0);(void)n;// 主线程while(true){char buf[64];snprintf(buf, sizeof(buf), "0x%x", tid);cout << "i am main thread " << "tid: " << buf << endl;sleep(1);}return 0;
}

 原因:线程出现了异常,OS就会发送信号到进程中,这个信号是发送给进程整体的,所以所有线程都会退出。
3️⃣ 缺乏访问控制
 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
 4️⃣ 编程难度提高
 编写与调试一个多线程程序比单线程程序困难得多
2.5 线程异常
1️⃣ 单线程如果出现除零或野指针问题导致线程崩溃,进程也会跟着崩溃。
 2️⃣ 因为进程具有独立性,导致其他进程最多只是对该进程只读但是不能写。而线程共用的是一个进程的地址空间,线程与线程之间的数据可以互相访问,当一个线程数据出错了,操作系统对该线程发信号,发信号只能发送给该线程对应的进程,进程跟着崩溃了,导致进程内的所有数据被释放,该进程内的其他线程也跟着销毁了(因为线程的数据是进程给的)。所以一个线程崩溃就会导致整个进程崩溃,这也造成了线程的健壮性降低的原因。
2.6 进程与线程间的关系

相关文章:
 
【linux】多线程概念详述
文章目录一、线程基本概念1.1 进程地址空间与页表1.2 页表结构1.3 线程的理解1.3.1 如何描述线程1.4 再谈进程1.5 代码理解1.5.1 原生库提供线程pthread_create1.6 资源共享问题1.7 资源私有问题二、总结2.1 什么是线程2.2 并行与并发2.3 线程的优点2.4 线程的缺点2.5 线程异常…...
【Java】P8 面向对象(3)方法 基本知识
面向对象 方法方法方法的声明权限修饰符返回值类型方法名形参列表方法体简单案例方法 方法 是对类或对象行为特征的抽象,用来完成某个功能的操作。方法的目的 是为了实现代码复用,减少冗余,简化代码;方法不能独立存在,…...
js中null和undefined的区别
js中null和undefined的区别?这也是一个常见的js面试题 相同点 1,都是基本类型。 2,做判断值都是false。 !!null false // true !!undefined false // true不同点 1,诞生时间null在前,undefined在后。因为js作者Brendan-Eic…...
 
【Linux】linux中的c++怎么调试?gdb的介绍和使用。
背景1.1.前提知识程序的发布方式有两种,debug模式和release模式Linux gcc/g出来的二进制程序,默认是release模式 要使用gdb调试,必须在源代码生成二进制程序的时候, 加上 -g 选项windows上的调试方法有区别吗?1.调试思路是一样的2…...
 
提升Python代码性能的六个技巧
文章目录前言为什么要写本文?1、代码性能检测1.1、使用 timeit 库1.2、使用 memory_profiler 库1.3、使用 line_profiler 库2、使用内置函数和库3、使用内插字符串 f-string4、使用列表推导式5、使用 lru_cache 装饰器缓存数据6、针对循环结构的优化7、选择合适算法…...
VI的常用命令
VI的常用命令 文章目录VI的常用命令vi/vim是什么?VI普通模式命令VI编辑模式命令VI指令模式vi/vim是什么? VI是Unix操作系统和类Unix操作系统中最通用的文本编辑器 VIM编辑器是从VI发展出来的一个性能更强大的文本编辑器。可以主动的将字体颜色辨别语法…...
 
【数据结构】万字深入浅出讲解单链表(附原码 | 超详解)
🚀write in front🚀 📝个人主页:认真写博客的夏目浅石. 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝 📣系列专栏:C语言实现数据结构 💬总结:希望你看完…...
 
无线WiFi安全渗透与攻防(五)之aircrack-ng破解WEP加密
系列文章 无线WiFi安全渗透与攻防(一)之无线安全环境搭建 无线WiFi安全渗透与攻防(二)之打造专属字典 无线WiFi安全渗透与攻防(三)之Windows扫描wifi和破解WiFi密码 无线WiFi安全渗透与攻防(四)之kismet的使用 aircrack-ng破解WEP加密 1.WEP介绍 其实我们平常在使用wifi的时…...
 
MySQL中事务的相关问题
事务 一、事务的概述: 1、事务处理(事务操作):保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit…...
推荐算法再次踩坑记录
去年搞通了EasyRec这个玩意,没想到今年还要用推荐方面的东西,行吧,再来一次,再次踩坑试试。1、EasyRec训练测试数据下载:git clone后,进入EasyRec,然后执行:bash scripts/init.sh 将…...
 
STM32 (十五)MPU6050
简介前言一、MPU6050简介MPU6050是一款性价比很高的陀螺仪,可以读取X Y Z 三轴角度,X Y Z 三轴加速度,还有内置的温度传感器,在姿态解析方面应用非常广泛。下面是它在淘宝上的参数图产品尺寸产品参数产品原理图:二、硬…...
使用yarn,依赖报各种错误怎么办
使用 yarn^3.x 版本时,默认并不会安装包到 node_modules,因为 yarn3.x 是即插即用的,也就是说如果你下载过这个包,yarn只会生成一个 Png文件,然后将包的路径 link 到下载过的地方,这样可以省去很多时间。而…...
面试官:rem和vw有什么区别
"rem" 和 "vw"的区别 "rem" 和 "vw" 都是用于网页设计的CSS单位。 "rem" 是相对于根元素的字体大小来计算的单位,即相对于 "html" 标签的字体大小。例如,如果 "html" 标签的字…...
【GPT-4】GPT-4 相关内容总结
目录 编辑 官网介绍 GPT-4 内容提升总结 GPT-4 简短版总结 GPT-4 基础能力 GPT-4 图像处理 GPT-4 技术报告 训练过程 局限性 GPT-4 风险和应对措施 开源项目:OpenAI Evals 申请 GPT-4 API API的介绍以及获取 官网介绍 官网:GPT-4 API候…...
 
5.springcloud微服务架构搭建 之 《springboot集成Hystrix》
1.springcloud微服务架构搭建 之 《springboot自动装配Redis》 2.springcloud微服务架构搭建 之 《springboot集成nacos注册中心》 3.springcloud微服务架构搭建 之 《springboot自动装配ribbon》 4.springcloud微服务架构搭建 之 《springboot集成openFeign》 目录 1.项目…...
 
【工作中问题解决实践 七】SpringBoot集成Jackson进行对象序列化和反序列化
去年10月份以来由于公司和家里的事情太多,所以一直没有学习,最近缓过来了,学习的脚步不能停滞啊。回归正题,其实前年在学习springMvc的时候也学习过Jackson【Spring MVC学习笔记 五】SpringMVC框架整合Jackson工具,但是…...
 
香港服务器遭受DDoS攻击后如何恢复运行?
 您是否发现流量异常上升?您的网站突然崩溃了吗?当您注意到这些迹象时,可能是在陷入了DDoS攻击的困境,因而,当开始考虑使用香港服务器时,也应该考虑香港服务器设备受DDoS攻击时,如何从中恢复。 在 DDoS 攻击香港…...
 
【Hive】配置
目录 Hive参数配置方式 参数的配置方式 1. 文件配置 2. 命令行参数配置 3. 参数声明配置 配置源数据库 配置元数据到MySQL 查看MySQL中的元数据 Hive服务部署 hiveserver2服务 介绍 部署 启动 远程连接 1. 使用命令行客户端beeline进行远程访问 metastore服务 …...
IP-GUARD如何强制管控电脑设置开机密码要符合密码复杂度?
如何强制管控电脑设置开机密码要符合密码复杂度? 7 可以在控制台-【策略】-【定制配置】,添加一条配置,开启系统密码复杂度检测。 类别:自定义 关键字:bp_password_complexity 内容:1 效果图:...
剑指 Offer II 031. 最近最少使用缓存
题目链接 剑指 Offer II 031. 最近最少使用缓存 mid 题目描述 运用所掌握的数据结构,设计和实现一个 LRU(Least Recently Used,最近最少使用) 缓存机制 。 实现 LRUCache类: LRUCache(int capacity)以正整数作为容量 capacity初始化 LRU缓…...
 
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...
 
聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...
 
为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?
在建筑行业,项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升,传统的管理模式已经难以满足现代工程的需求。过去,许多企业依赖手工记录、口头沟通和分散的信息管理,导致效率低下、成本失控、风险频发。例如&#…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
HTML前端开发:JavaScript 常用事件详解
作为前端开发的核心,JavaScript 事件是用户与网页交互的基础。以下是常见事件的详细说明和用法示例: 1. onclick - 点击事件 当元素被单击时触发(左键点击) button.onclick function() {alert("按钮被点击了!&…...
 
视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
 
Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
 
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
 
【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
