多线程(一)
文章目录
在了解多线程之前,我们先聊聊进程
进程
而了解进程前,我们还需聊聊操作系统~
操作系统
简单来理解就是搞管理的软件。
- 对下:管理好各种硬件设备
- 对上:要给应用程序通过稳定的运行环境
操作系统的"内核态 " 是操作系统里面最核心的功能模块, 硬件的驱动程序都是在系统内核中执行的~ 内核需要给很多应用程序提供支持~
一个程序在运行中,可能是用户态在工作,也可能是内核态在工作~
一个操作系统 = 内核 + 配套的应用程序
而操作系统的内核涉及很多关键性概念,进程只是其中一个,这里作为引入,所以接下来详细聊聊进程~
进程
每个应用程序运行于现代操作系统之上时,操作系统会提供一种抽象,好像系统上只有这个程序在运行,所有的硬件资源都被这个程序在使用。这种假象是通过抽象了一个进程的概念来完成的,进程可以说是计算机科学中最重要和最成功的概念之一。
进程process/task
,通俗来看,一个已经跑起来的程序,就是进程。
以上都是在执行的进程,对于windows
这个系统来说,一开机,就会有百八十个进程。
每个进程想要执行,就需要消耗一定的系统资源(也就是硬件资源)
每个进程,都是系统资源分配的基本单位~
那么进程在系统中是如何进行管理的呢?
从两个角度来看:
- 描述 使用类/结构体,把被管理的一个对象,各个属性都表示出来
- 组织 使用数据结构,把这些表示出来的对象串起来~(这是为了后续的增删改查)
而在系统中专门有一个结构体(操作系统内核是使用C/C++)描述进程的属性,而这些结构体统称为 "进程控制块PCB"
使用这些PCB描述进程属性,一个进程就可以使用一个或者多个PCB来表示~
在系统中,就会使用类似于双向链表这样的数据结构来组织多个PCB,其相关操作:
- 创建新的进程: 创建 PCB 并且把 PCB 插入到链表中
- 销毁进程: 就是将 PCB 从链表上删除并且释放
- 展示进程列表: 就相当于遍历链表
要明确认识进程详细的特性的话,可以进一步聊聊 PCB 里面的属性
PCB 是一个非常庞大的结构体,包含很多属性,在Linux
中,PCB 被称之为 task_struct
-
pid
:进程的身份标识每一个进程都会有一个
pid
,同一时刻,不同进程之间的pid
是不同的 -
内存指针
每一个进程在运行的时候,都会分配一定的内存空间
也就是说,这个进程的内存空间,具体是在哪里,以及分配的内存空间有哪些部分,每部分的职责都是会有一组指针来进行区分的~
举一个典型的例子,进程的内存空间,需要有专门的区域存储要执行的指令,以及指令以来的数据~同时还需要存储一些运行时产生的临时数据
举个现实生活的例子:老师布置作业,那么同学们在执行任务之前,就需要将作业记录到自己的作业登记本上面~
-
文件描述符表
描述了进程持有的"硬盘资源"是什么样子的~
类似于顺序表这样的数据结构,有很多元素和文件有关 => 硬盘有关
一个进程也需要是涉及到硬盘操作,就需要按照文件的方式来操作,当前进程关联了哪些文件,都能操作哪些文件,就是通过文件描述符表来看的
进程持有的cpu
资源如何体现?
- 分时复用
- 并发
早期的操作系统,是一个单任务的操作系统,就是同一时刻只有一个进程能运行,运行下一个进程,就会退出上一个~
一个进程要执行,就是需要cpu
来执行上面的指令,早期的电脑,还是单核cpu
,一个cpu
核心,同一时刻,只能执行一个进程的指令
这里我们可以这样类比
cpu
核心:舞台- 进程:演员
- 指令:剧本(剧本上有很多幕)
那么我们就可以这样来理解分时复用
、并发
- 分时复用:很多演员,轮流上去演剧,每个演员演完一幕就下来了,腾出地方,给下一个演员去演
- 并发:只要演员的轮转速度过快,此时就好像这些演员同时在表演一样
如果两个进程同时在两个cpu
核心上,微观上也是"同时执行",这个情况称为"并行 "
一个cpu
核心上,通过快速轮转调度 的方式,执行多个进程,宏观上是"同时执行",微观上有先有后,这个情况称为"并发"
咱们作为普通的程序猿平时也不会具体区分并发还是并行.从编程角度来说,底层是并发还是并行,对代码没啥影响...平时也就会统一使用"并发"来代指并行和并发.
PCB属性
PCB中引入了一些属性,用来支持操作系统实现进程调度的效果~~
- 进程的状态
- 进程的优先级
- 进程的上下文
- 进程的记账信息
为了容易理解这四个属性,我们给定一个场景:
假设你是一个性感好看有才华的妹妹
有很多男的追求你:
1. 有钱
2. 185黑皮体育生大帅哥
3. 死舔狗
但是没有一个人兼备,但是小孩子才做选择,你全都要,所以你不得不拜罗志祥为师,成为时间管理大师,因此你需要合理规划时间,来确保这三个人不会同时出现(分时复用),同时和三个男的谈恋爱🤣🤣🤣🤣🫣
进程的状态
默认情况下:这三男的,随叫随到,呼之即来,挥之即去
这种情况称之为"就绪状态 ",进程时刻准备好,去cpu
上执行
而就绪状态,具有两种情况:
- 进程在
cpu
上执行 (你现在在和A约会) - 虽然没执行,但是时刻准备着去
cpu
上执行 (今天日程安排没有和B约会,但是只要你想要了,call B B立刻出现🥵🥵🥵)
但是如果有一天,在你的安排里面,今天是和C约,但是C去出差了(他也想立刻回来舔你,但是不赚钱怎么舔啊~),此时称这为"阻塞状态"
某个进程,某种执行条件不具备,就导致这个进程暂时无法参与cpu
的调度执行
进程的优先级
操作系统在调度多个进程的时候并非是一视同仁,有些进程会给更高的优先级,优先调度
我的电脑上同时运行 L O L (优先级更高)和q q(更低)
更好的调配系统资源
把钱花在刀刃上~~
消息晚收一会都不是大问题~~
用上面的场景来看就是:
你在排时间表的时候ABC三个小哥,分配的时间并非是均等的
A我最喜欢分的时间最多,B其次C只会舔,舔腻歪了,分的最少
进程的上下文
引入场景:
有一天,你和A约会,A说,下个月,咱们一起去巴厘岛度假吧~~我说好啊,他说那你准备准备~~
第二天,你和B约会,B说,下个月,他的妈妈过生日,他不知道该买啥礼物,想让我帮忙.你说好啊.他说,那你准备准备
针对A,你做的准备是:准备护照,准备一套性感的泳衣🫣🫣🫣
针对B,你准备一套首饰~~
我又和A约会,A问我,你准备的咋样了?
我又和B约会,B问我,你准备的咋样了?
这时候如果你两个准备颠倒过来了,那就死了,穿帮咯
务必要避免穿帮的情况~~
你需要搞小本本,把每次和这些人约会的状态,具体有哪些事情没搞完,需要下次继续搞,都明确记录好并且在下次约会之前,要温习一下
这也就是上下文~
正经点来看就是:
进程从cpu
离开之前,需要保存现场,把当前cpu
中各种寄存器的状态,都记录到内存中
等到下次进程回到cpu
上执行的时候,此时就可以把保存的这些寄存器的值,恢复回去.进程就会沿着上次执行到的位置,继续往后执行.
也就是:读档、存档
既然讲到这里,有点CPU寄存器那味了~
CPU寄存器
CPU中有些寄存器,属于没有特定含义,就只是用来保存运算的中间结果的,还有些寄存器,是有特定含义,特定作用的.
-
保存当前执行到哪个指令 (程序计数器)
是一个2字节/4字节/8字节整数
这个整数存的是一个内存地址
内存地址:程序下一条要执行的指令所在的位置:exe
里面就包含了指令和数据,把exe
运行起来,操作系统就会把指令和数据加载到内存中.(内存地址)
CPU
就会先从内存中取指令,然后再执行指令.初始情况下,程序计数器就指向进程指令的入口(简单粗暴的想象成是main方法)
每次取完一条指令,程序计数器的值都会自动更新,
默认情况下,直接指向下一条(顺序执行)
但是如果遇到跳转类指令
jmp,jcmp,call
),就会被设置成跳转到的地址... -
维护栈相关的寄存器
通过这一组(一般是两个)维护当前程序的"调用栈"
而 栈,也是一块内存,这个内存里就保存当前这个程序方法调用过程中,一系列的关系(也包含局部变量和方法参数...)
ebp
始终指向栈底
esp
始终指向栈顶.修改esp
的值就可以实现"入栈"/"出栈"(push指令完成上述操作)
有了这个才知道一个方法执行完毕之后,回到哪里执行.
-
其他的通用寄存器了.往往是用来保存计算的中间结果的.
比如说
10+20+30+40
假设在算完10+20之后,还没来得及算后面,进程调度走了,就需要把保存10+20的寄存器的值给备份到上下文中
实际上一个cpu
里面的寄存器也没多少,几十个字节到几百个字节,数据不多,保存好保存,恢复也好恢复 。一般都是直接将这些寄存器都一股脑打包进内存即可,也就是PCB
进程的记账信息
通过优先级机制,对不同的进程分配了不同权重的资源,
有可能会出现极端的情况,所有的资源都给某个进程,其他进程一点都没捞着~~
比如说:一周4天拍给A,2天拍给B,1天放假,C??
如果我一直不搭理C,C对我的热情就会降低可能就会去舔别的女神了.适当的得给他点甜头,得能让它看到点希望~~
我就需要统计一段时间给ABC分配总天数如果发现C特别少,就需要适当的补偿
记账信息,会记录当前进程持有cpu
的情况(在cpu
执行多久了)就可以作为操作系统调度进程的参考依据~
虚拟地址空间
虚拟地址空间是操作系统中用来给每个进程分配内存的一种技术。它为每个进程提供了一个独立的虚拟内存空间,使得每个进程可以同时存在于主存储器中,而不会相互干扰。
虚拟地址空间的作用主要有以下几个方面:
-
内存隔离:每个进程都有自己的虚拟地址空间,使得它们之间的内存彼此独立,相互隔离。这样可以保护进程的私密数据,防止进程之间的相互影响。
-
资源管理:虚拟地址空间使操作系统可以更有效地管理内存资源。它允许操作系统灵活地分配和回收内存,并且可以将实际的物理内存分配给不同的进程。
-
内存映射:虚拟地址空间允许进程将文件或其他设备映射到其地址空间中。这种机制使得进程可以直接访问这些文件或设备的内容,从而避免了繁琐的文件和设备操作。
-
内存保护:虚拟地址空间允许操作系统对进程的内存进行保护。通过设置权限位和访问控制列表,操作系统可以限制进程对内存的访问,防止错误的内存访问或恶意行为。
总而言之,虚拟地址空间提供了一种使得每个进程都拥有自己独立的地址空间的机制,提高了系统的安全性、可靠性和资源管理效率。
不看那么多字,我们用一张图理解:
至此,通过上述方式,把进程之间给隔离开了.如果某个需求中,确实就需要让多个进程相互配合,此时就不好搞了.
此处就需要引入新的机制,来实现进程之间的通信,
具体的实现方式有很多.都是要借助一个公共空间,完成数据的交互但是每个方式的核心思想都是一样的。
在现今多核CPU
的时代,为了实现并发编程,我们需要引入多个进程。
多进程,实现并发编程,效果也是非常理想的。
但是,多进程编程模型,也有明显的缺点,进程太重量,效率不高。
- 创建一个进程,消耗时间比较多
- 销毁一个进程,消耗时间也比较多
- 调度一个进程消耗时间也比较多...
-
这些时间都是消耗在申请资源上的,进程是资源分配的基本单位。而分配内存,就是一个大工作~
-
因为操作系统内部有一定的数据结构,把空闲的内存分块管理好.
当我们去进行申请内存的时候,系统就会从这样的数据结构中
如果需要频繁的创建/销毁进程,这个时候年销就不能忽视了
找到一个大小合适的空闲内存,返回给对应的进程。、
-
这里虽然通过此处的数据结构,可以一定程度提高效率,整体来说,管理的空间比较多,相比之下还是一个耗时操作~
-
如果需要频繁的创建/销毁进程,这个时候开销就不能忽视了
(这一点在早期服务器开发中是非常常见的情况,
C++CGI
技术,就是这样一种基于多进程的方式实现网站后端)
为了解决这个问题,所以下面我们引出线程这块知识~
线程
线程也叫做"轻量级进程"。
创建、销毁、调度线程都比操作进程更快~
但是线程不能独立存在,而是要依附于进程(也即是说进程包含线程),进程可以包含一个线程,也可以包含多个线程。
一个进程,最开始的时候,至少要有一个线程这个线程负责完成执行代码的工作,也可以根据需要,创建出更多的线程,从而使当前实现"并发编程"的效果~
前面谈到进程调度,前面的讨论都是基于"一个进程里只有一个线程"的情况。
实际上,一个进程中,是可以有多个线程的每个线程,都是可以独立的进行调度的~~
每一个线程,也有状态,优先级,上下文,记账信息...
一个进程,使用PCB表示,一个进程可能使用一个PCB表示,也可能使用多个PCB表示.每个PCB对应到一个线程上,除此之外,前面谈到的pid
都是相同的,内存指针,文件描述符表,也是共用一份的~
上述结构,决定了线程的特点:
- 每个线程都可以独立的在
CPU
上面调度执行 - 同一个进程的多个线程间,共用同一份内存空间和文件资源
所以说,创建线程的时候,不需要重新申请资源了,直接复用之前已经分配给进程的资源,省去了资源分配的开销,于是创建效率就更高了。
进程中包含线程 => 一个进程由多个 PCB 共同表示 => 每个PCB就用来表示一个线程 => 每个线程都有自己的状态,上下文,优先级,记账信息 =>
每个线程都可以独立的去 CPU 上调度执行 => 这些 PCB 共用了同样的内存指针和文件描述符表 => 创建线程(PCB)不需要重新申请资源 => 创建/销毁效率都更高了
- 进程是资源分配的基本单位
- 线程是调度执行的基本单位
一个系统中,可以有很多进程,每个进程,都有自己的资源,一个进程中,可以有很多线程,每个线程都能独立调度,共享内存/硬盘资源
下面我们用图片例子来进一步理解:
设定情景:一张桌子只能坐8个鸡哥,鸡哥们吃坤腿~
线程与进程的区别
言归正传,我们来聊聊线程与进程的区别
-
进程包含线程,一个进程里面可以有一个线程,也可以有多个线程
-
进程和线程,都是用来实现并发编程场景的.但是线程比进程更轻量,更高效
-
同一个进程的线程之间,共用同一份的资源(内存+硬盘),这就会省去了申请资源的开销
-
进程和进程之间,是具有独立性的,如果一个进程挂了,是不会影响到别人.
但是线程和线程之间(前提是同一个进程内),是可能会相互影响的.(线程安全问题+线程出现异常)
-
进程是资源分配的基本单位,线程是调度执行的基本单位
Java进行多线程编程
线程是操作系统的概念.操作系统提供了一些APl,可以操作线程
Java针对上述系统API
进行了封装.(跨平台)
所以我们只需要使用好Thread
类,创建Thread
对象就可以进一步操作系统内部的线程了。
java
package Thread;
class MyTread extends Thread{
@Override
public void run() {
//这个方法就是线程的入口方法
System.out.println("hello world");
}
}
//演示创建线程
public class Demo1 {
public static void main(String[] args) {
Thread t = new MyTread();
//start 和 run 都是 Thread 的成员
//run 只是描述了线程的入口(线程主要做什么任务)
//start 则是真正调用了系统API,在系统中创建线程,让线程再调用run
t.start();
}
}
至此多线程(一)前沿知识先到这,接下会持续更新,敬请期待~