线程 与 进程
- 一. 进程
- 1. 操作系统
- 2. 什么是进程/任务(Process/Task)
- 3. 为什么要有进程
- 3. 操作系统怎样管理进程
- 4. 进程控制块抽象(PCB Process Control Block)
- 5. 进程调度
- 6. 内存分配 —— 内存管理(Memory Manage)
- 7. 进程间通信(Inter Process Communication)
- 二. 线程
- 1. 线程是什么
- 2. 为什么要有线程
- 3. 进程和线程的区别与联系
- 4. Java 的线程 和 操作系统线程 的关系
- 5. 第一个多线程程序
一. 进程
1. 操作系统
操作系统: 一组做计算机资源管理的软件的统称。
目前常见的操作系统有:Windows系列、Unix系列、Linux系列、OSX系列、Android系列、iOS系列、鸿蒙等。
操作系统的定位:
蓝色部分:系统调用、操作系统内核、驱动程序都属于操作系统。
操作系统由两个基本功能:
- 对下, 管理好各种设备。
- 对上, 给各种软件提供一个稳定的运行环境 。
2. 什么是进程/任务(Process/Task)
进程/任务: 就是跑起来的程序(运行起来的可执行文件),进程是操作系统对一个正在运行的程序的一种抽象,
换言之,可以把进程看做程序的一次运行过程。同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位。
3. 为什么要有进程
因为操作系统支持多任务了,程序员也就需要并发编程了,通过进程,完全可以实现并发编程。
3. 操作系统怎样管理进程
- 先描述一个进程,明确一个进程上面的一些属性,此处的描述其实用的是 C 语言中的结构体(操作系统主要由 C/C++ 实现),这个结构体称为 PCB (Process Control Block) 进程控制块。
- 组织若干进程,使用一些数据结构把这些 PCB 放到一起。
Linux 的典型实现就是使用 双向链表 把每个进程的 PCB 连接起来。
创建进程:先创建 PCB, 然后把 PCB 加到双向链表中。
销毁进程:找到链表上的 PCB,然后从链表中删除。
(我们平时查看任务管理器其实就是遍历这个链表。)
4. 进程控制块抽象(PCB Process Control Block)
计算机内部要管理任何现实事物,都需要将其抽象成一组有关联的、互为一体的数据。在 Java 语言中,我们可以通过类/对象来描述这一特征。
// 以下代码是 Java 代码的伪码形式
class PCB {
// 进程的唯一标识 —— pid;
// 进程关联的程序信息,例如哪个程序,加载到内存中的区域等
// 分配给该资源使用的各个资源
// 进度调度信息
}
-
PID:
进程 ID -> 进程的身份证号。 -
内存指针:
指明这个进程要执行的代码/指令以及这个进程执行中所依赖的数据存放在内存的哪块区域。
运行可执行文件时,操作系统会把这个 可执行文件(包含进程执行的二进制指令以及数据)加载到内存中变成进程。 -
文件描述符表:
程序运行过程中,经常要和文件打交道,进程每打开一个文件,就会在文件描述符表上添加一项。
(这个表可视为一个数据,里面的每个元素又是一个结构体,对应一个文件信息。)
一个进程只要启动,不管代码中是否包含打开/操作文件的代码,都会默认打开三个文件(系统自动打开)
(1) 标准输入流
(2) 标准输出流
(3) 标准错误流 -
状态:
描述当前进程接下来应该怎样调度
(1) 就绪状态:随时可以去 CPU 上执行
(2) 阻塞状态:暂时不可去 CPU 上执行, 比如正在进行 IO 密集型操作,如读写数据。
(3) 运行状态:正在 CPU 上执行 -
优先级:
根据进程的优先级来决定:先给哪个进程分配时间,后给哪个进程分配时间,以及给哪个进程分配的时间多, 给哪个进程分配的时间少。 -
上下文:
表示 该进程上次被调出 CPU 时,当时该进程的执行状态,下次该进程上 CPU 时,可恢复之前的状态,然后继续往下执行。
进程被调度出 CPU 之前,需要把 CPU 寄存器里面的数据都保存到内存中(就是该 上下文字段),相当于存档了,下次 再被调上 CPU 执行时,就从刚才的内存中恢复这些数据到寄存器中,相当于时读档了,存档+读档存储的信息就被称为 上下文。 -
记账信息:
统计了每个进程都分别执行了多久,分别执行了哪些指令,分别排队等了多久,从而给进程调度提供知道依据。
以上只是几个核心属性。
这样,每一个 PCB 对象,就代表着一个实实在在运行着的程序,也就是进程。
操作系统再通过这种数据结构,例如线性表、搜索树等将 PCB 对象组织起来,方便管理时进行增删查改的操作。
5. 进程调度
就是 操作系统考虑 CPU 资源如何给各个进程分配。
6. 内存分配 —— 内存管理(Memory Manage)
操作系统对内存资源的分配,采用的是空间模式 —— 不同进程使用内存中的不同区域(虚拟地址空间),互相之间不会干扰。
虚拟地址空间: 程序中获取到的内存地址并不是真正的物理内存地址,而是经过一层抽象,虚拟出来的地址。
操作系统上同时运行多个进程,如果某个进程出现 bug 崩溃了,不会影响到其他进程(进程之间相互独立),就是因为内存按照虚拟地址空间分配。
一旦进程访问越界了,MMU 硬件设备就会向进程反馈,从而终止进程的非法操作。
7. 进程间通信(Inter Process Communication)
进程是操作系统进行资源分配的最小单位,这意味着各个进程互相之间是无法感受到对方存在的,这就是操作系统抽象出进程这一概念的初衷,这样便带来了进程之间互相具备 “隔离性(Isolation)”。
但现代的应用,要完成一个复杂的业务需求,往往无法通过一个进程独立完成,总是需要进程和进程进行配合地达到应用的目的,如此,进程之间就需要有进行“信息交换“的需求。进程间通信的需求就应运而生。
目前,主流操作系统提供的进程通信机制有如下:
- 管道
- 共享内存
- 文件
- 网络
- 信号量
- 信号
其中,网络是一种相对特殊的 IPC 机制,它除了支持同主机两个进程间通信,还支持同一网络内部非同一主机上的进程间进行通信。
二. 线程
1. 线程是什么
线程: 一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码.
比如如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。
为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
2. 为什么要有线程
首先, “并发编程” 成为 “刚需”.
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
- 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
其次,用进程实现并发编程有点问题:
- 频繁创建/销毁进程成本较高
- 频繁的调度进程成本也是较高的
解决:
- 使用进程池
进程池 虽然能解决上述问题, 提高效率,但是同时也有问题,进程池里面闲置的进程不使用的时候,也在占用并消耗系统资源,这样消耗的系统资源就太多了。 - 使用多线程
线程比进程更加轻量,每个线程也可以执行一段代码(一个任务),也能并发编程,并且创建、销毁、调度线程的成本也比调度进程低很多。
为什么线程比进程更轻量?进程重在哪?
- 进程重在资源的申请和释放,创建、销毁、调度进程就得重新分配 内存、硬盘、文件、网络等资源,对于资源的申请和释放本身就是比较低效的操作。
- 而对于线程,线程包含在进程中,一个进程中的多个线程共用一份资源,只有创建第一个线程和释放最后一个线程时才涉及到资源的重新分配,中间线程的创建和销毁都不涉及到资源的重新分配。
多加一些线程,执行效率会不会进一步提高 ?
- 一般来说会, 但是也不一定。
- 因为多个线程共用一份资源(主要是指内存和文件描述符表,在线程 1 里面 new 的对象,在线程 2、3 里面也可以用,在线程 1 打开的文件在线程2、3 里面也可以直接使用),资源有限,如果线程太多,那么线程之间对资源的竞争会进一步加剧,如果时间全部浪费到竞争资源上面了,执行效率可能不升反降,整体的执行效率就被拖慢了。
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程” (Coroutine)
3. 进程和线程的区别与联系
-
进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
-
进程和线程都是为了解决并发编程这样的场景, 但是使用进程进行并发编程的话会有一些问题,频繁的创建、调度和销毁线程时效率低,相比之下,线程更轻量,创建、释放以及调度的效率高,只有创建第一个线程和释放最后一个线程时才需要资源的申请或释放。
-
进程是系统分配资源的最小单位,线程是系统 CPU 调度的最小单位。
-
进程之间具有独立性,每个进程有自己独立的虚拟地址空间,一个进程挂了,不会影响其他进程。一个进程中的多个线程共用同一块内存空间,一个线程挂了,可能影响其他线程,甚至导致整个进程崩溃。
4. Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
5. 第一个多线程程序
- 每个线程都是一个独立的执行流
- 多个线程之间是 “并发” 执行的
class Main {
private static class MyThread extends Thread {
@Override
public void run() {
// 每个线程的任务都是循环 10 次
for (int i = 1; i <= 10; i++) {
// 打印当前线程的名字
System.out.println(Thread.currentThread().getName() + "正在运行!");
try {
// 休眠 1 s
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
// main 线程也循环 10 次
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + "正在运行!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
输出:
使用 JDK 中自带的 jconsole 工具就可以查看对应的线程:
注意:
- 一个进程可以同时占用多个 CPU 核心,因为线程才是 CPU 调度的基本单位。
- 并行:微观上同一时刻,多个核心同时运行。
- 并发:微观上,同一时刻,一个核心上只能运行一个进程, 但是它能够对进程进行足够快的切换,比如,一个 CPU 核心先运行 QQ,再运行一下 腾讯视频,再运行一下浏览器,只要切换的足够快,宏观上感知不到,人看起来就像这几个进程在同时运行。
好啦! 以上就是进程与线程的基本讲解,希望能帮到你 !
评论区欢迎指正 !