进程、线程、协程
进程
基本概念
程序
我们电脑中的程序其实就是已经写好的一些代码,然后通过编译成一些指令,存储在电脑的硬盘中,需要我们手动启动。
进程
进程是动态执行程序的过程,是操作系统资源分配的最小单元。
程序与进程的区别
1、程序是永久存储在磁盘的(除非手动删除或其他异常情况),进程存在于内存中,在程序启动时创建进程、程序结束之后销毁进程。
2、一个程序可以有多个进程,例如同时打开多个聊天APP
3、进程需要占用系统资源,程序不需要。
进程的组成
在创建进程时,操作系统会维护一个进程控制块(Process Control Block, PCB),这个进程控制块是进程存在的唯一标志,里边包含进程的所有信息,例如
1、进程标识:这个进程的唯一编号,进程ID(PID),以及创建这个进程的进程编号,父进程ID(PPID);
2、进程状态信息:当前这个进程所处的运行状态,运行状态分别有运行态、阻塞态、就绪态等
3、CPU上下文信息:当这个进程被阻塞时,PCB会保存CPU的寄存器的值,这样下次切换回来的时候这个进程就能被恢复
4、资源分配:记录内存空间资源的地址,打开的文件列表,网络等信息
5、优先级信息:主要用于操作系统做进程调度时使用,因为优先级更高的进程会先被执行
进程状态切换
进程会有多种不同的状态,分别是:
创建态
创建进程,操作系统为其分配资源,初始化PCB等。通常由父进程通过系统调用创建(如 Linux 的fork()
、Windows 的CreateProcess()
),完整步骤是:
- 操作系统为新进程分配唯一 PID 和 PCB。
- 分配资源(如内存空间,复制父进程的代码段、数据段,或加载新程序)。
- 初始化 PCB 信息(状态设为就绪,设置优先级等)。
- 将新进程加入就绪队列。
就绪态
这个状态下进程已经获得了进程运行的完备条件,只是还没有获取到CPU时间片,需要等待操作系统的调度即可切换到运行态
运行态
此时进程正在CPU上执行指令,在单CPU系统中,同一时刻只有一个进程运行
阻塞态
进程由于等待某一事件(IO操作,获取资源等)被暂停,PCB保存CPU上寄存器的值,然后等待被另一进程唤醒
终止状态
进程执行完毕或者被强制终止,释放进程资源,PCB可能暂时保留,供父进程查询状态之后删除。通常发生在程序执行完毕、被其他进程强制终止(如kill
命令)、发生错误(如除以零)等。
完整的步骤是:
- 释放进程占用的资源(内存、文件句柄等)。
- 更新 PCB 状态为终止状态。
- 通知父进程(通过信号或状态码),等待父进程 “回收”(如 Linux 的
wait()
系统调用),最终 PCB 被删除。
进程状态之间会发生切换,切换的时机如下:
- 就绪 → 运行:操作系统调度该进程,分配 CPU 资源。
- 运行 → 就绪:时间片用完(如抢占式调度中,高优先级进程到来),进程被暂停并回到就绪队列。
- 运行 → 阻塞:进程执行 I/O 请求(如读取文件),进入阻塞状态等待结果。
- 阻塞 → 就绪:I/O 操作完成,进程被唤醒,进入就绪队列等待调度。
进程调度算法
操作系统在进行任务调度时会根据设定的算法从就绪队列中取出待执行的进程,调度算法的核心是为了提升CPU利用率、缩短响应时间。常见的调度算法有:
先来先服务(FCFS):按进程到达就绪队列的顺序调度,简单但可能导致 “长进程阻塞短进程”(如一个耗时 1 小时的进程先到达,后面的 1 分钟进程需等待 1 小时)
短作业优先(SJF):优先调度运行时间最短的进程,能提高吞吐量,但需要预知进程运行时间,且可能导致长进程 一直得不到调度。
时间片轮转(RR):为每个进程分配固定的 CPU 时间片(如 10ms),时间片用完后进程回到就绪队列,轮流执行。适用于交互式系统(如桌面操作系统),保证响应速度。
优先级调度:为进程分配优先级,高优先级进程优先执行。可分为静态优先级(创建时固定)和动态优先级(运行中根据情况调整,如长时间未执行的进程优先级提高,避免饥饿)。
进程上下文切换
当操作系统从一个进程切换到另一个进程执行时,需要进行上下文切换,步骤如下:
- PCB 中保存当前进程的 CPU 上下文(寄存器值)。
- 根据调度算法从就绪队列中选择一个待执行的进程。
- 恢复该进程的 CPU 上下文(从其 PCB 中读取寄存器值)到 CPU 中。
- 跳转到该进程的程序计数器(PC)指向的指令,开始执行。
上下文切换会消耗 CPU 时间(保存 / 恢复数据、更新内核数据结构等),频繁切换会降低系统效率。因此,操作系统需平衡调度频率和切换开销。
进程间通信方式(IPC)
常见的进程间通信方式有:
管道
管道方式有匿名管道和命名管道。核心通信机制相同(基于 FIFO 队列,先进先出),但命名管道通过文件系统提供了跨进程的标识能力。
匿名管道
匿名管道是由pipe()
系统调用创建的临时管道,仅存在于内存中,没有文件系统路径,生命周期随进程结束而销毁,适合具有“血缘”关系的进程,如父子进程、兄弟进程之间的单向通信;
进程创建管道后会获得两个文件描述符,fd[0]
(读端)和fd[1]
(写端)。数据从写端写入,从读端读出,遵循 “先进先出(FIFO)” 规则。
当读端无数据时,读操作会阻塞;写缓冲区满时候,写操作会阻塞;数据被一次性读取之后,会从管道中删除,无法重复读取。
命名管道
命名管道是一种特殊类型的文件,通过文件系统中的路径名来标识,可以被系统中的任意进程访问。进程通过文件路径打开命名管道(open()
),获得读 / 写文件描述符后,通信方式与匿名管道一致(FIFO 顺序,半双工)。
即使创建管道的进程退出,只要管道未被所有进程关闭,数据仍可留存。
命名管道的实现本质上是内核中的一个缓冲区,遵循 “先进先出(FIFO)” 规则,其通信模式由打开方式决定:
-
一个进程以 “写” 方式打开管道,另一个以 “读” 方式打开,数据从写端流向读端。
-
若要实现双向通信,必须创建两个命名管道 :一个用于进程 A→进程 B,另一个用于进程 B→进程 A。
消息队列
消息队列是内核维护的一种结构化数据缓冲区,进程通过发送/接收消息(包含类型和数据)实现通信。
工作原理
- 进程通过
msgget()
创建或获取消息队列(用键值key
标识)。 - 发送进程通过
msgsnd()
向队列发送消息(消息包含类型mtype
和数据mtext
)。 - 接收进程通过
msgrcv()
从队列读取消息,可指定接收特定类型的消息(按mtype
筛选)。 - 消息队列独立于发送 / 接收进程存在,进程退出后消息可留存,直到被显式删除或系统重启
优缺点
消息包含类型和数据,接收方可按需筛选,避免数据混乱;发送 / 接收可设置非阻塞模式,避免无限等待;内核对消息队列的总大小、单条消息大小有上限(可通过内核参数调整)。
支持任意进程间通信,数据有结构,可按类型接收,生命周期独立于进程。但是数据传递需要经过内核拷贝,有大小限制
适用于分布式系统中的任务调度,如调度进程向工作进程发送带类型的任务消息;日志收集(多个进程向日志进程发送不同级别的日志消息)。
共享内存
共享内存是效率最高的 IPC 方式,它允许多个进程直接访问同一块物理内存区域,无需数据拷贝。
工作原理
1、 创建共享内存,进程通过shmget()
创建或者获取共享内存段(用键值key
标识),指定大小权限
2、将共享内存段映射到自己的虚拟地址空间中,之后可以像访问本地地址一样读写操作
3、多个进程映射统一段共享内存之后,直接读写内存实现数据交换
4、进程退出前需要撤销映射,最后删除共享内存段,释放内存资源。
共享内存本身不具备同步机制,多个进程同时读写可能导致数据冲突,需要结合信号量和互斥锁进行同步
优缺点
因为是直接做的地址映射,所以数据无需在内核缓冲区中转,实现零拷贝;可以支持较大数据量;并且独立于进程,需要手动删除。
优点是:效率最高,适合高频大量的数据通信,但是缺点是缺少同步机制,安全性不高。
信号量
用于进程同步,也就是进程间同步与互斥,控制对共享资源的访问,核心操作主要有
- P 操作(Wait):请求资源。若计数器 > 0,计数器减 1 并继续;若 = 0,进程阻塞等待。
- V 操作(Signal):释放资源。计数器加 1,若有阻塞进程,唤醒其中一个。
当计数器为0或1时,本质就是互斥锁,控制资源的独占访问,同一时间只能有一个进程访问共享内存;当计数器>1时,控制多个进程共享有限资源。
工作原理
1、进程首先创建或者获取一个信号量集(一组信号量,用键值key
标识)
2、执行p/v
操作,传入操作数组(指定对哪个信号执行P.V,以及操作的值)
3、进程完成资源访问后,删除信号量集
优缺点
本质上不传递数据,仅协调进程行为,避免资源竞争;P/V操作由内核保证原子性,不会被中断。
套接字
套接字是跨网络或本地进程通信的通用接口,最初用于网络通信,后来扩展到本地进程间通信(UNIX 域套接字)。
工作原理
网络套接字通过IP地址+端口号标识,UNIX域套接字,用文件系统路径标识,仅用于本地进程。
通信的流程:创建套接字(socket()
)→ 绑定地址(bind()
)→ 监听 / 连接(listen()/connect()
)→ 读写数据(recv()/send()
)→ 关闭套接字(close()
)
套接字支持多种协议,比如TCP/UDP等
特点
套接字支持本地和网络进程通信,用于网络编程,也可以通过TCP实现可靠通信,但是TCP是字节流,需要手动处理粘包,UDP和UNIX域套接字可以传输数据包,有边界
内存映射文件(Memory-Mapped Files)
内存映射文件通过将磁盘文件映射到进程的虚拟地址空间,实现多个进程共享文件内容(间接实现内存共享)。
工作原理
- 进程通过
mmap()
将文件映射到虚拟内存,文件数据被加载到物理内存。 - 进程读写映射区域时,内核自动将修改同步到磁盘文件(或通过
msync()
手动同步)。 - 多个进程映射同一个文件后,通过读写映射区域实现通信,修改会反映到所有映射进程和磁盘文件。
数据同步到磁盘,进程退出后数据不丢失(区别于共享内存);支持超大文件(超过物理内存大小,内核通过页交换管理);缺少同步机制,需要添加信号量等工具配合
优缺点
适合数据持久化,适合需要长期保存的共享数据;支持大文件;依赖文件系统,同步到磁盘有开销;需手动处理同步问题。
最典型的场景是:
- 大型数据库的共享缓存(多个进程通过映射数据库文件高效访问数据)。
- 跨进程的日志文件实时读写(多个进程向同一日志文件写入,通过映射减少 I/O 次数)。
总结
IPC 方式 | 效率 | 数据量 | 适用场景 | 典型场景示例 | |
---|---|---|---|---|---|
匿名管道 | 中 | 小 | 亲缘进程,简单单向通信 | Shell 命令管道(ls | grep ) |
|
命名管道 | 中 | 小 | 任意进程,简单通信 | 本地服务与客户端通信 | |
消息队列 | 中 | 中 | 结构化数据,按类型接收 | 任务调度、日志收集 | |
共享内存 | 极高 | 大 | 高频、大数据量通信 | 高性能计算、实时系统 | |
信号量 | 高 | 无(同步用) | 资源同步与互斥 | 共享内存 / 文件的访问控制 | |
信号 | 高 | 极小(通知) | 事件通知、异常处理 | 进程终止、异常捕获 | |
套接字 | 中 – 低 | 任意 | 本地 / 网络通信,跨主机 | Web 服务、跨机进程通信 | |
内存映射文件 | 中 – 高 | 大 | 持久化共享数据,大文件 | 数据库缓存、日志共享 |
线程(Thread)
线程是操作系统能够进行运算调度的最小单位,通常由进程创建,是进程中的实际运作单位,一个进程可以有多个线程,这些线程共享共享进程的资源(内存空间,文件描述符等),但是拥有独立的程序计数器、栈空间和寄存器。
核心特点
线程的创建、销毁和切换的开销都小于进程,因为线程共享进程的资源,无需进行复杂的资源复制;
每个线程都有自己的程序计数器(记录下一条要执行的指令)、栈(存储局部变量和函数调用信息)还有寄存器集合,操作系统可以独立调度每一个线程;
在多核多处理器中,多个线程可以并发执行,在单处理器中通过时间片轮转实现并发效果。
线程的缺点
如果一个线程阻塞,那么其他线程也会阻塞;并且由于线程都共享同一个进程的资源,可能会出现数据竞争关系,对于同一块数据读写需要同步机制保证线程安全问题。
线程分类
线程分为用户线程和内核线程。
用户线程
在用户态由线程库函数创建的线程,操作系统内核不知道线程的存在。
用户线程的维护都是由响应的进程通过线程库函数维护的,并且用户线程的切换也是由线程库函数来完成的,不需要内核态/用户态之间的切换,所有速度快;每一个进程都可以有自己的独立的线程调度算法。
用户线程的缺点是:如果其中某个线程阻塞了,那么整个进程都会阻塞;当一个线程运行后,除非它主动交出CPU使用权,否则这个进程中的其他线程就无法运行,那么也就无法充分利用多核CPU的优势。
内核线程
在操作系统内核中创建的线程,是由内核创建、管理的。操作系统的内核会维护进程和线程的上下文信息(PCB TCB),在整个线程的创建和切换过程中,都是系统调用内核函数的方式进行,系统开销较大。
与用户线程相比,其中某个线程阻塞,其他线程依然可以运行,不会阻塞,时间片会直接分配给各个线程,所以能够充分利用多核处理器的性能
线程的生命周
线程的生命周期与进程的生命周期类似,都是具有创建、就绪、运行、阻塞、终止几种不同的状态,状态切换的条件基本类似
线程同步与互斥
由于在一个线程中,多个线程同时操作共享数据时,会出现数据竞争的问题,并且可能导致数据不一致的情况,常见的解决方案都是添加同步机制:
1、加锁:
确保同一个时间只有一个线程会操作共享资源,那么就可以通过加锁和解锁来控制临界区资源的访问
在进入临界区之前加锁,如果锁已经被其他线程获取,就会阻塞等待获取锁;离开临界区时必须释放锁,让其他线程获取锁。
2、互斥量
互斥量(Mutual Exclusion)是一种特殊的同步原语,核心功能是 “互斥”—— 保证多个线程(或进程)对共享资源的排他性访问,本质上是一种 “二进制锁”(状态只有 “锁定” 和 “未锁定”)
互斥量与锁的共同点都是实现排他性的访问,在同一时间只允许一个线程进入临界区;但是不同点在于互斥量是操作系统内核管理的,在内核实现,可以跨进程适用,但是锁只是用户态实现,只能在一个进程内的线程中使用
同时互斥量只有获取它的线程才能释放它,其他线程无法释放
3、信号量:
信号量通过一个“计数器”控制同时访问共享资源的线程数量,允许最多N个线程同时进入临界区。
核心原理是PV操作:P(等待,Proberen,意为 “测试”)和V(释放,Verhogen,意为 “增加”)
P操作
:计数器减 1,若结果≥0,线程可继续执行;若 < 0,线程阻塞等待。V操作
:计数器加 1,若有线程阻塞,唤醒其中一个。
从信号量可以衍生出二进制信号量和计数信号量
- 二进制信号量:计数器只能是 0 或 1,功能等价于互斥量(允许 1 个线程访问)。
- 计数信号量:计数器可设为任意正整数
N
,允许N
个线程同时访问。
4、条件变量
条件变量是一种用于线程间 “通信” 的同步机制,允许线程在 “条件不满足” 时阻塞等待,直到其他线程 “通知” 条件满足后再唤醒执行。它必须与锁(或互斥量)配合使用。
工作原理是:
当线程获取锁之后,检查条件是否满足,如果满足了则执行临界区的操作,释放锁,如果不满足则会调用`wait()
释放锁,并且阻塞等待
当其他线程修改了条件之后,会唤醒某一个阻塞的线程,或且唤醒全部线程之后,被唤醒的线程会重新获取锁并再次检查条件。
最主要的操作有:wait()
,notify_one()
,notify_all()
四种同步机制对比
机制 | 核心功能 | 典型场景 | 关键特性 |
---|---|---|---|
锁(Lock) | 单线程排他访问临界区 | 单个共享资源的串行操作 | 用户级,轻量,可重入 |
互斥量(Mutex) | 跨线程 / 进程的排他访问 | 跨进程资源共享(如硬件设备) | 内核级,有所有权 |
信号量(Semaphore) | 限制 N 个线程同时访问 | 资源池并发控制(如连接数限制) | 计数灵活,支持多线程并发 |
条件变量(Condition Variable) | 线程间条件通信 | 生产者 – 消费者、依赖关系场景 | 需配合锁,基于条件唤醒 |
轻量级进程(little weight process, LWP)
轻量级进程(Lightweight Process ,LWP)是操作系统内核支持的一种用户态线程抽象,本质上是内核级线程(Kernel-Level Thread)在用户态的映射。它是介于传统进程(Heavyweight Process)和纯用户级线程之间的中间层,既保留了进程的部分隔离性,又具备线程的轻量特性。
LWP 由操作系统内核直接管理,每个 LWP 都对应一个内核级线程(KLT),并拥有独立的内核调度实体(如简化的进程控制块 PCB)。同一进程内的多个 LWP 共享进程的地址空间、文件描述符等资源,但各自拥有独立的寄存器状态、栈空间(用户栈和内核栈)和调度优先级。
LWP工作原理
1、内核支持
每一个LWP都与一个内核级线程(KLT)绑定,内核会通过调度KLT调度LWP,LWP的创建、销毁和切换都需要内核通过系统调用管理,内核也会位每个LWP维护状态(就绪、运行、阻塞)
2、 资源共享
在同一个进程中的LWP共享进程的所有资源,所以在多个LWP之间通信可以直接通过共享内存交互
3、调度运行机制
每一个LWP对应一个内核线程,多个LWP可以被调度到不同的CPU核心上并行执行,可以充分利用多核性能,并且当一个LWP因为IO操作阻塞时,内核会调度其他LWP执行,避免整个进程被阻塞。
4、用户态与内核态的交互
用户程序通过线程库与LWP交互,线程库负责向内核请求创建LWP,绑定用户级线程等操作,隐藏了内核实现。
LWP的线程模型
LWP可以有多种线程模型
1、一对一模型
一个用户级线程映射到一个内核级线程上,每个用户线程都有自己的内核线程,所以线程之间的运行互不干扰,互不阻塞
2、 多对一模型
多个用户线程映射到一个内核级线程上,这种模型的优点在于轻量,创建和切换的开销小,但是一个线程的阻塞会导致其他线程也阻塞
3、多对多模型
多个用户级线程映射到多个内核级线程(LWP)上,用户级线程有线程库调度到LWP上执行,LWP由内核调度到CPU上执行,特点是,用户级线程的轻量特性(创建、切换快),同时又可以多核利用并且单个线程阻塞不会影响到全局。
LWP应用场景
- 高并发:对多核处理器利用较多的场景如:服务器程序、科学计算等,LWP的并行能力可以提升并发效能
- I/O密集型任务:网络服务器(处理大量的客户端连接),单个LWP处理IO是其他LWP可以继续服务新的请求,避免阻塞
- 跨平台线程:一些线程库在部分系统上基于LWP实现,例如(java,lang.Thread pytho的threading)
- 实时:需要严格针对优先级进行调度的场景,LWP的内核级调度可以保证其实时性
LWP与Thread 、Process的区别
特性 | 传统进程(Heavyweight Process) | 用户级线程(ULT) | 轻量级进程(LWP) |
---|---|---|---|
内核感知性 | 内核完全感知(有独立 PCB) | 内核不感知(由用户库管理) | 内核完全感知(有简化 PCB) |
资源共享 | 独立地址空间,不共享资源 | 共享所属进程资源 | 共享所属进程资源 |
调度单位 | 内核直接调度 | 由用户库在单个内核线程上调度 | 内核直接调度(依赖内核线程) |
阻塞影响 | 单个进程阻塞不影响其他进程 | 一个线程阻塞可能导致整个进程阻塞 | 单个 LWP 阻塞不影响其他 LWP |
多核利用 | 可利用多核(多进程并行) | 无法利用多核(单内核线程) | 可利用多核(多个内核线程并行) |
创建 / 切换开销 | 大(需复制地址空间等资源) | 小(用户态操作) | 中等(内核操作,但资源共享) |
协程(Coroutine)
协程又叫微线程。
协程的本质就是在单线程内实现的“伪并发”机制,多个协程共享同一个进程的内存空间和线程资源,但是也有用自己独立的执行上下文(局部变量、程序计数器、栈帧等),程序通过主动切换协程的上下文,实现多个任务的 “交替执行”,从外部看类似多线程并发,但实际运行在单个线程内(或绑定到少数线程上)。
工作原理
协程的核心是“上下文切换”与“协作调度”,工作的主要流程如下:
1、上下文切换
每个协程拥有独立的执行上下文(包括局部变量、栈指针、程序计数器等)。当协程主动让出执行权(使用yield
,await
关键字),程序会保存当前协程的上下文状态,当该协程再次获得执行权时,再恢复之前的上下文,从暂停处继续执行
2、协作式调度
协程的调度完全由用户代码控制,没有内核。当协程执行到IO操作时,主动通过await
让出CPU,让其他协程执行,等待IO完成之后,再由调度器唤醒协程继续执行。
3、时间循环
多数协程框架依赖“事件循环”实现调度,事件循环是一个无限循环,负责管理所有协程的状态(运行、就绪、挂起),并在协程让出执行权之后,从就绪队列选择下一个协程执行。
适用场景
协程主要低开销、高并发
1、IO密集型任务
爬虫(处理大量HTTP请求)、API服务(处理多客户端请求)、数据库操作等,这类任务大部分时候都在等待IO响应,协程可以在这段时间内让出CPU,让其他任务执行,大幅提升CPU利用率
2、高并发场景
需要同时处理数百万任务的场景,协程占用的内存很小,可以支持很大的并发
3、异步编程
传统异步编程依赖回调函数(Callback Hell),而协程通过async/await
等语法,将异步代码写成同步风格,大幅降低编程复杂度。
协程优缺点
优点
- 协程的创建和开销极小,内存占用低,可以支持百万级并发
- 在用户态实现上下文切换,无需内核参与,切换速度快
- 可以将异步编程用同步的风格编写
- 共享线程资源,不需要内核资源(PCB,内核的栈帧等)
缺点
- 协程运行在单个线程内,无法直接利用多核,需要和多进程/多线程配合适用
- 若协程执行CPU密集型任务或未正确处理阻塞操作,则会阻塞整个线程,导致其他协程也无法执行
- 需要语言方面去支持
- 协程的切换由用户支持,所以可能定位较为困难
协程与进程、线程的核心区别
特性 | 进程(Process) | 线程(Thread) | 协程(Coroutine) |
---|---|---|---|
调度者 | 操作系统内核 | 操作系统内核 | 用户程序(开发者控制) |
调度方式 | 抢占式(内核强制切换) | 抢占式(内核强制切换) | 协作式(需主动让出执行权) |
上下文切换开销 | 大(涉及地址空间、资源切换) | 中(内核态切换,保存寄存器等) | 极小(用户态切换,仅保存局部状态) |
资源共享 | 独立地址空间,不共享资源 | 共享进程资源 | 共享所在线程 / 进程资源 |
并发能力 | 支持多核并行 | 支持多核并行 | 单线程内并发(需配合多线程利用多核) |
阻塞影响 | 单个进程阻塞不影响其他进程 | 单个线程阻塞不影响其他线程 | 一个协程阻塞会导致整个线程阻塞 |
数量上限 | 数百至数千(受内存限制) | 数万(受内核资源限制) | 数十万至数百万(内存占用极小) |