程序(Program) 是静态的指令
进程是程序的一次执行
进程被创建时,操作系统会在内核空间中创建一个PCB(Process Control Block)内容包括:
1. 分配一个唯一的PID,记录所属用户的UID
2. 分配了多少内存,正在使用什么IO设备,正在使用哪些文件
3. 进程运行情况:CPU使用时间,网络流量使用情况,磁盘IO流量
PCB是进程存在的唯一标志
线程
进程帮助实现了程序的并行执行,但如果一个进程需要同时做多件事,比如qq聊天,传输文件需要同时进行, 我们可以创建多个进程,缺陷是进程的创建和销毁开销大,跨进程通信复杂;优点是一个进程崩溃不会影响其他进程。
为了提高性能,引入线程
因为只能串行的执行一系列指令,线程是CPU调度的基本单位。早期的进程(Process)其实就是现在的线程(Thread),而现代的进程则变成了一个“更高层级的资源容器”
线程的实现方式
内核级线程和用户级线程的根本区别是 管理线程的权利在进程手中还是内核手中;或者说,调度线程是否需要切换到内核态
User-level Thread
线程库(Thread Library):对于用户级线程,线程库相当于运行在用户态的操作系统,实现调度。
线程库是一个函数库,提供函数。线程池(Thread Pool) 在Thread Library基础上加入复用线程而不是销毁,是一个
一般包含:
- TCB (Thread Control Block) :类似PCB, 记录每个线程的
1. TID
2. 状态(运行、就绪、阻塞)
3. 栈指针(ESP/RSP)
4. 寄存器快照
5. 优先级
早期操作系统不支持线程,线程由线程库实现,操作系统不知道线程的存在。线程库创建,销毁和调度线程,例如:把内核分配给进程的资源分配给线程,简化逻辑如下
int main(){1. 线程管理由进程完成
2. 线程切换没有切换到内核态
应用
P这里调度的逻辑写在asyncio库中
优点
1. 不需要切换状态,性能高
缺点
1. 如果其中一个线程被阻塞,这个进程中其他线程都会被阻塞
Kernel-level Thread
和用户级线程的根本区别是 管理线程的权利全部在进程的主线程手中还是内核手中
1. 创建,销毁,调度由操作系统完成
2. 线程切换需要切换到内核态
场景:
- 线程执行“阻塞操作”(如 read() 磁盘文件或 recv() 网络数据) 引发Trap:
1. Trap
2. 如果需要等待数据,操作系统会把进程放入阻塞队列
3. 进程中其他不需要这个数据的线程也被阻塞了
解决方法:Non-blocking I/O 也就是协程,java中的虚拟线程。
我们使用另一种read(),当内核缓存中没有,内核不会阻塞线程,而是返回一个错误码,然后让线程继续运行。
澄清:主线程只是启动其他线程的线程,并不一定负责调度
优点
1. 一个线程被阻塞,不会影响其他线程。因为CPU调度算法下,你被阻塞会被放入阻塞队列,即使没有,也只会浪费分配给你的CPU时间片
缺点:
1. 需要变态,效率降低
import threading
import time
def worker(id):
for i in range(3):
# time.sleep(1) 是一个“真阻塞”操作
# 它会告诉操作系统内核:“我没事干了,把我挂起吧”
# 内核收到后,会主动把 CPU 切换到另一个线程
print(f"内核级线程 {id}: 正在干活...")
time.sleep(1)
# 主线程逻辑
print("【主线程】: 开始创建内核级线程")
t1 = threading.Thread(target=worker, args=(1,))
t2 = threading.Thread(target=worker, args=(2,))
t1.start()
t2.start()
# 主线程调用 join(),进入阻塞状态,等待内核唤醒
t1.join()
t2.join()
print("【主线程】: 任务全部完成")多线程模型
一对一模型
原始的KLT
多对一模型
就是原始的ULT
多对多模型
n个内核线程对应m个用户级线程