进程,线程,协程
进程、线程、协程(面试完整版 + 高频考点)
一、基础概念
1. 进程(Process)
操作系统资源分配的最小单位
- 程序:磁盘上的静态文件;进程:程序运行起来的实例。
- 每个进程独立拥有:CPU 时间片、内存、文件描述符、堆、栈等资源。
- 进程之间相互隔离,一个进程崩溃不会影响其他进程。
- 进程切换开销很大:需要保存 / 恢复整个内存上下文、页表、寄存器等。
适用场景:CPU 密集型任务(计算、大数据运算)
2. 线程(Thread)
CPU 调度执行的最小单位(轻量级进程)
- 线程依附于进程,一个进程至少有 1 个主线程。
- 同一个进程内所有线程共享进程资源(全局变量、文件句柄等)。
- 线程切换只需要切换少量寄存器,开销远小于进程。
- 多线程并发,存在线程安全问题,需要锁(Lock)。
适用场景:IO 密集型(网络请求、文件读写、数据库操作)
3. 协程(Coroutine)
用户态的轻量级线程,由程序调度,操作系统无感知
- 不是操作系统原生线程,在单线程内实现并发。
- 切换只需要保存少量上下文,开销极小。
- 遇到 IO 阻塞时主动让出 CPU,不用操作系统切换。
- 无需锁机制,单线程执行,天然线程安全。
适用场景:超高并发 IO 密集型(爬虫、异步接口、高并发网络服务)
二、三者层级关系
进程 → 线程 → 协程
1 个进程可以包含多个线程;
1 个线程可以包含多个协程。
三、核心对比表
表格
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度者 | 操作系统内核 | 操作系统内核 | 用户程序(代码手动调度) |
| 资源 | 独立资源,进程间隔离 | 共享进程资源 | 共享线程资源 |
| 切换开销 | 最大 | 中等 | 极小 |
| 并发方式 | 多进程并行 | 多线程并发 | 单线程异步并发 |
| 安全问题 | 无共享,安全 | 共享全局变量,需要加锁 | 单线程执行,无需锁 |
| 适合场景 | CPU 密集型 | IO 密集型 | 高并发 IO 密集型 |
| Python 限制 | 无 GIL 限制 | 受 GIL 限制,无法利用多核 | 单线程,无法利用多核 |
四、Python 经典难点:GIL 全局解释器锁
- GIL 是 CPython 独有锁,同一时刻一个进程内只能有一个线程执行 CPU 运算。
- 多线程在 CPU 密集场景下无法利用多核,本质串行,效率甚至不如单线程。
- IO 密集型时线程阻塞会释放 GIL,多线程可以提升效率。
- 想要利用多核:用多进程(每个进程有独立 GIL)。
场景选型总结
- CPU 密集型(矩阵运算、数据分析、加密) → 用 多进程 multiprocessing
- 普通 IO 密集型(少量爬虫、文件读写) → 用 多线程 threading
- 高并发 IO(上万并发爬虫、异步 Web) → 用 协程 asyncio
五、进程间通信 / 线程间通信
1. 线程间通信
- 共享全局变量(不安全,必须加
threading.Lock) - 队列:
queue.Queue(线程安全)
2. 进程间不能直接共享全局变量(内存隔离)
常用方式:
multiprocessing.Queue队列- Pipe 管道
- Manager 共享字典、列表
- 共享内存
六、协程关键知识点
-
协程是非抢占式调度:只有协程主动让出 CPU(await IO)才会切换;进程 / 线程是抢占式:操作系统时间片到了强制切换。
-
三大实现方式:
- 生成器 yield 实现简易协程
async/await(Python3.5+ 官方标准协程)- gevent 第三方猴子补丁协程
-
协程缺点:
- 一旦某个协程是 CPU 密集型阻塞,整个线程被卡死,其他协程无法调度;
- 无法利用多核,想要多核 + 协程:多进程 + 每个进程内开协程。
七、高频面试简答题
1. 并发和并行区别
- 并行:同一时刻多个任务同时执行(多核 CPU,多进程 / 多线程)
- 并发:同一时间段交替执行,宏观同时,微观串行(单核多线程、协程)
2. 什么是僵尸进程、孤儿进程
- 僵尸进程:子进程先结束,父进程没调用 wait () 回收退出状态,子进程残留 PCB。
- 孤儿进程:父进程提前退出,子进程被系统 init 进程收养,不会成为僵尸。
3. 线程死锁产生四个必要条件
- 互斥条件
- 请求与保持
- 不可剥夺
- 循环等待
解决:破坏任一条件,比如统一锁的获取顺序、设置超时释放锁。
八、三者简单代码示例
1. 多线程
python
运行
import threading
def task():
print("线程任务")
t = threading.Thread(target=task)
t.start()
2. 多进程
python
运行
from multiprocessing import Process
def task():
print("进程任务")
if __name__ == "__main__":
p = Process(target=task)
p.start()
3. 协程 async/await
python
运行
import asyncio
async def task():
await asyncio.sleep(1)
print("协程执行")
asyncio.run(task())
九、面试常问坑点
- Python 多线程为什么不能利用多核?GIL 限制。
- GIL 什么时候释放?CPU 时间片用完、遇到 IO 阻塞、手动 sleep。
- 协程需要加锁吗?单线程调度,同一时间只有一个协程运行,不需要互斥锁。
- 进程崩溃会带走里面所有线程;单个线程崩溃会导致整个进程崩溃。
进程、线程、协程 10 道高频面试真题(含答案 + 解析)
一、基础问答类
第 1 题:简单说说进程、线程、协程三者区别与联系?
参考答案:
- 进程:操作系统资源分配最小单位,进程间内存相互隔离,切换开销最大,一个进程至少包含一个主线程,适合 CPU 密集型任务,可利用多核。
- 线程:CPU 调度最小单位,依附进程,同进程内线程共享进程资源,切换开销中等;CPython 受 GIL 限制,多线程无法实现 CPU 密集并行,适合 IO 密集场景,多线程共享变量存在线程安全问题,需要加锁。
- 协程:用户态轻量级线程,单线程内实现并发,由程序手动调度,切换只需要保存少量上下文,开销极小;非抢占式调度,遇到 IO 主动让出 CPU,天然线程安全无需互斥锁,适合高并发 IO 场景,无法利用多核。
层级关系:一个进程可以有多个线程,一个线程可以运行多个协程。
第 2 题:并发和并行的区别?
参考答案:
- 并行:多核 CPU 下,多个任务同一时刻同时执行,多进程可以实现并行。
- 并发:单核 CPU 下,多个任务交替抢占时间片执行,宏观上同时运行,微观上串行;多线程、协程都属于并发。
第 3 题:什么是 GIL?GIL 带来什么影响?什么时候释放 GIL?
参考答案:
- GIL 全称全局解释器锁,是 CPython 解释器的一把互斥锁,保证同一时刻一个进程内只有一个线程执行 CPU 代码。
- 影响:
- 多线程无法利用多核做 CPU 密集型并行计算,效率低下;
- IO 密集场景下线程阻塞会释放 GIL,多线程可以提升效率。
- GIL 释放时机:
- 线程遇到 IO 阻塞(网络、文件、sleep)自动释放;
- 线程 CPU 执行时间片耗尽强制释放;
- 手动调用特定方法主动释放。
二、场景选型题
第 4 题:CPU 密集、IO 密集分别用什么方案?为什么?
参考答案:
- CPU 密集(大数据计算、加密、图像处理)→ 多进程
原因:规避 GIL 限制,每个进程拥有独立 GIL,可以利用多核并行计算;缺点是进程切换开销大,占用内存多。
- 普通 IO 密集(文件读写、数据库、少量接口请求)→ 多线程
原因:IO 阻塞时释放 GIL,多线程并发执行,线程切换开销小,开发简单。
- 高并发 IO(爬虫、高并发 web 服务)→ 协程 asyncio
原因:协程切换开销远小于线程,上万并发场景资源占用极低,单线程调度无需锁。
第 5 题:协程为什么不需要互斥锁?
参考答案:
协程属于非抢占式调度,同一线程内任意时刻只有一个协程在运行,只有当前协程主动遇到 IO、await 让出执行权时,才会切换其他协程,不会出现多个协程同时修改共享变量的情况,因此天然线程安全,不需要加互斥锁。
三、线程安全 & 锁类高频题
第 6 题:多线程共享全局变量为什么不安全?怎么解决?
参考答案:
- 不安全原因:线程执行过程中可能被 CPU 时间片强制切换,一个变量自增操作会被拆分为多条指令,切换后导致数据错乱。
- 解决方案:
- 使用
threading.Lock互斥锁,保证同一时间只有一个线程执行临界区代码; - 使用线程安全工具:
queue.Queue队列; - 使用局部变量代替全局变量。
第 7 题:死锁产生的四个必要条件?如何避免死锁?
参考答案:
- 四个必要条件:
① 互斥条件:资源同一时间只能被一个线程占用;② 请求并保持:线程持有已有资源,同时请求新资源不释放旧资源;③ 不可剥夺:已占用资源不能被其他线程强制抢走;④ 循环等待:多个线程形成首尾循环等待资源的闭环。
- 避免方案:
- 破坏循环等待:所有线程按照固定顺序申请锁;
- 设置锁超时时间,超时自动释放;
- 尽量减少嵌套锁使用。
四、进程通信 & 面试易错坑题
第 8 题:进程间为什么不能直接共享全局变量?进程间有哪些通信方式?
参考答案:
- 每个进程拥有独立的内存空间,操作系统做了内存隔离,直接使用全局变量无法在多个进程间同步数据。
- 常用通信方式:
multiprocessing.Queue安全队列;- Pipe 管道,适合两个进程双向通信;
- Manager 管理器,共享列表、字典等数据结构;
- 共享内存、文件、socket。
补充:线程间通信可以用全局变量 + 锁、
queue.Queue。第 9 题:僵尸进程、孤儿进程是什么?怎么处理僵尸进程?
参考答案:
- 僵尸进程:子进程先执行结束退出,父进程没有调用
wait()/waitpid()回收子进程的退出状态,子进程 PCB 进程控制块残留在内核中,占用系统资源。 - 孤儿进程:父进程提前退出,子进程会被操作系统的 init 进程收养,init 会自动回收资源,孤儿进程无害。
- 处理方式:父进程调用 wait 方法回收、设置信号处理机制自动回收子进程。
五、进阶综合题
第 10 题:协程能不能利用多核?想要多核 + 高并发 IO 怎么实现?
参考答案:
- 原生协程运行在单线程中,无法直接利用多核 CPU。
- 实现方案:使用多进程 + 每个进程内部开启协程,既通过多进程规避 GIL 利用多核,又借助协程实现单进程内高并发 IO,最大化并发性能。
附加 3 道手写代码面试题(简要考点)
- 用多线程实现多个任务并发,加锁保证变量安全
- 用多进程实现 CPU 密集计算,使用进程队列收集结果
- 用 asyncio 实现简单协程并发爬虫
浙公网安备 33010602011771号