【Python】 多线程 & 多进程&协程 常见题 + 应用场景整理

核心前提:GIL全局解释器锁,CPython中同一时刻只有1个线程执行Python字节码。

  • 多线程:适合 IO密集型(网络请求、文件读写、数据库)
  • 多进程:适合 CPU密集型(图像运算、模型推理、数值计算),每个进程独立GIL

一、基础概念题

1. 多线程和多进程区别?

  1. 内存空间
    • 线程:同进程内,共享内存,全局变量共享;线程之间通信简单,但存在竞态条件,需要锁。
    • 进程:独立内存空间,互不共享;通信成本高(管道、队列、共享内存)。
  2. GIL影响
    • 多线程:CPU密集任务,GIL限制,无法多核并行,只能并发;IO阻塞时GIL释放,所以IO场景好用。
    • 多进程:每个进程独立GIL,可以多核真正并行,但创建销毁开销大。
  3. 开销
    • 线程轻量,创建成本低;进程重量级,fork/创建开销大。
  4. 崩溃影响
    • 一个线程崩溃,整个进程挂掉;一个进程崩溃,不影响其他进程。

一句话总结:IO密集用threading;CPU密集用multiprocessing

2. GIL是什么?为什么有GIL?

GIL(Global Interpreter Lock)全局解释器锁,CPython的一把互斥锁,保证同一时间只有一个线程执行Python字节码。

  • 目的:简化CPython内存管理(引用计数非线程安全)
  • 释放时机:
    1. 线程遇到IO阻塞(sleep、网络请求、读文件)→ 主动释放GIL
    2. 执行一定字节码计数,强制切换

注意:GIL不是线程安全锁,不能替代lock!多个线程修改共享变量依然会数据错乱。

3. 并发、并行区别

  • 并发(concurrent):单核CPU快速切换任务,宏观看起来同时跑;多线程属于并发。
  • 并行(parallel):多核CPU,同一时刻多个任务真正同时执行;多进程可以并行。

4. 什么是线程安全?竞态条件?

竞态条件:多个线程同时读写同一个共享变量,执行时序不确定,结果错乱。

python 复制代码
# 非线程安全示例
count = 0
def add():
    global count
    for _ in range(100000):
        count +=1

count +=1 不是原子操作(读→+1→写回),多线程不加锁会丢更新。

解决:threading.Lock()。

5. 死锁产生条件、怎么避免

死锁4个必要条件:

  1. 互斥:资源独占
  2. 持有并等待:线程拿着资源,同时申请新资源
  3. 不可剥夺:资源不能被强制抢走
  4. 循环等待:线程互相等待对方资源

避免方案:统一资源申请顺序、设置超时、减少嵌套锁。

二、模块 & API 相关考题

6. threading 常用组件

  • threading.Thread:创建线程
  • threading.Lock:互斥锁
  • RLock:可重入锁(同一线程可以多次acquire)
  • Semaphore:信号量,控制并发数量(爬虫限制并发请求)
  • Event:线程间事件通知(一个线程发信号,另一个等待)
  • Condition:条件变量,锁+等待通知,生产者消费者

7. multiprocessing 常用组件

  • Process:进程
  • Queue:进程安全队列(推荐用来进程通信)
  • Pipe:双向管道,两个进程通信
  • Manager:创建共享对象 list/dict(底层是单独管理进程,性能一般)
  • Pool:进程池,复用进程,避免频繁创建销毁(最常用)

⚠️ 进程之间不能直接用全局变量共享,必须用Queue/Pipe/共享内存。

8. 进程池 Pool 和手动创建Process区别

  • Pool:预先创建一批进程,任务投递复用进程,适合大量短任务,减少fork开销
  • Process:每次新建进程,适合长驻任务;任务量大时开销很高

9. 线程池 concurrent.futures.ThreadPoolExecutor / ProcessPoolExecutor

高层封装,面试高频,简洁易用,支持submit+result()、map。

工程推荐优先用concurrent.futures,少手写底层threading。

10. 守护线程 daemon

  • daemon=True:守护线程,主线程退出时,守护线程直接被杀死,不等待执行完毕。
  • daemon=False(默认):主线程会等待所有非守护线程全部结束,才退出。

注意:守护线程不要执行文件/数据库写操作,可能中途被打断导致数据损坏。

三、通信与同步题

  1. 线程间通信方式
    共享全局变量(配合Lock)、Queue(threading.Queue线程安全队列)、Event、Condition

threading.Queue 内部自带锁,线程安全,推荐优先使用,不要裸写全局变量。

  1. 进程间通信IPC
    multiprocessing.Queue、Pipe、共享内存Value/Array、Manager、Socket

不要用普通全局变量,子进程是拷贝一份,修改互不影响。

四、异常、坑点高频题

  1. 多线程中time.sleep()会不会释放GIL?

    会,IO/睡眠阻塞时释放GIL,其他线程可以跑,所以爬虫多线程有效。

  2. 为什么CPU密集任务,多线程比单线程还慢?

    GIL频繁切换,线程切换带来额外开销,没有多核加速,切换损耗反而拖累速度。

  3. multiprocessing在Windows下必须if __name__ == "__main__":?

    Windows创建进程是spawn方式,不是fork,会重新导入模块,不加判断会递归无限创建进程。Linux/macOS fork模式可以省略,但代码规范建议统一加上。

  4. 进程池里的函数能不能用lambda?

    不推荐,pickle序列化问题,部分环境会报错;尽量用普通函数。

五、应用场景 & 工程选型

多线程适用场景(IO密集)

  1. 爬虫:并发请求http接口
  2. 文件批量读写、日志采集
  3. 数据库批量查询(等待DB返回)
  4. API网关并发接收请求

限制:CPU计算 heavy 的任务不要用

多进程适用场景(CPU密集)

  1. 图像批量处理:OpenCV、PIL图片解码、缺陷检测(你之前做的表面检测)
  2. 大模型预处理、向量批量编码(Embedding批量计算)
  3. 数值运算、信号处理
  4. 离线批量推理任务

不适合场景

  • 多进程:短小IO任务,进程创建开销远大于任务本身,得不偿失
  • 多线程:大量矩阵运算、模型推理等纯CPU计算

六、极简代码样例(面试手写)

1. ThreadPoolExecutor 线程池(IO密集)

python 复制代码
from concurrent.futures import ThreadPoolExecutor
import time

def task(x):
    time.sleep(0.5) # 模拟IO等待
    return x * 2

if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=4) as pool:
        res = pool.map(task, range(10))
    print(list(res))

2. ProcessPoolExecutor 进程池(CPU密集)

python 复制代码
from concurrent.futures import ProcessPoolExecutor

def heavy_task(x):
    s = 0
    for i in range(1000000):
        s += i
    return s + x

if __name__ == "__main__":
    with ProcessPoolExecutor(max_workers=4) as pool:
        ret = pool.map(heavy_task, range(8))
    print(list(ret))

3. 线程安全,Lock 保护共享变量

python 复制代码
import threading
count = 0
lock = threading.Lock()

def add():
    global count
    for _ in range(100000):
        with lock:
            count +=1

t1 = threading.Thread(target=add)
t2 = threading.Thread(target=add)
t1.start()
t2.start()
t1.join()
t2.join()
print(count)

七、面试对比速记表

项目 多线程 threading 多进程 multiprocessing
资源 共享同进程内存 独立内存空间
GIL 受GIL限制,CPU密集无法多核并行 每个进程独立GIL,可以多核并行
创建开销 小 大
通信 简单(Queue/共享变量+锁) IPC复杂,开销高
适用 IO密集:爬虫、DB、网络请求 CPU密集:图像、数值计算、批量推理
崩溃影响 线程崩,整个进程挂 单个进程崩溃隔离

八、延伸高频追问

  1. 协程asyncio和多线程对比?(协程用户态轻量,无线程切换开销,单线程内IO多路复用)
  2. 多进程的共享内存multiprocessing.Value/Array和Manager区别?
  3. 任务队列 Celery底层是多进程/多线程?
  4. 多线程写文件怎么保证不混乱?

九、协程 asyncio

协程:用户态轻量级任务,由程序自身调度,不是操作系统内核线程;单线程内并发,无内核线程切换开销。

  • 关键字:async / await,事件循环event loop
  • 适用:超高并发IO场景(上万HTTP请求、大量数据库查询)
  • 限制:遇到同步阻塞代码(time.sleep、requests、普通pymysql)会卡住整个事件循环,必须使用异步库(aiohttp、aiomysql)

面试题

  1. asyncio 和 threading 多线程的区别?
  • 协程:单线程,用户态调度,切换成本极低;没有锁竞争问题,但不能跑CPU密集任务;一旦有同步阻塞,整个循环卡死。
  • 多线程:OS内核调度,切换开销更大;可同时处理同步IO;受GIL约束。
  1. await 后面能跟什么?

    必须是可等待对象:coroutine、Task、Future。普通函数不能直接await。

  2. Task作用?

    asyncio.create_task():把协程提交到事件循环后台并发执行。

极简示例

python 复制代码
import asyncio

async def io_task(x):
    await asyncio.sleep(0.5) # 异步sleep,不阻塞事件循环
    return x*2

async def main():
    tasks = [asyncio.create_task(io_task(i)) for i in range(5)]
    res = await asyncio.gather(*tasks)
    print(res)

asyncio.run(main())

选型口诀:

少量IO并发 → ThreadPoolExecutor

海量IO并发(几千上万连接)→ asyncio

CPU密集图像/模型推理 → ProcessPool

十、进阶高频坑点

1. 多进程 Pickle 序列化问题

multiprocessing / ProcessPoolExecutor 在spawn模式下,任务函数、入参需要pickle序列化传递到子进程。

  • ❌ 不能用lambda、嵌套函数、部分动态类,容易报pickle error
  • ❌ 闭包捕获大对象,序列化会拷贝大量数据,内存暴涨
  • ✅ 方案:顶层普通函数;参数尽量传基础类型,不要传大对象

2. 线程池 max_workers 怎么设置?

  • ThreadPoolExecutor:IO密集,可设较大,一般 10~50;受下游服务QPS限制,不是越大越好,避免打爆DB/接口
  • ProcessPoolExecutor:CPU密集,一般等于CPU核心数,开太多会CPU抢占、上下文切换飙升,性能下降

3. 多进程共享内存:Value / Array vs Manager

  • Value/Array:真正共享内存,性能高,底层共享一块内存;适合简单数值、数组,需要手动加锁。
  • Manager().list/dict:单独启动一个管理进程,子进程通过IPC访问,性能差,方便,适合简单业务,大批量数据不要用。

4. 为什么多线程中,Queue 推荐优先使用?

threading.Queue内部已经封装锁,线程安全,生产者消费者模型首选,避免自己手写Lock造成死锁。

multiprocessing.Queue 是进程安全队列,底层管道实现。

5. 多进程下全局变量修改为什么不生效?

Linux fork:子进程启动时复制父进程内存(写时复制);修改变量会触发内存拷贝,父子变量互相独立 。

Windows spawn:完全重新导入模块,变量从头初始化。

👉 结论:多进程不要依赖全局变量传数据。

6. 锁:Lock vs RLock

  • Lock:互斥锁,同一线程不能acquire两次,会死锁。
  • RLock(可重入锁):同一个线程可以多次acquire,必须对应相同次数release;适合嵌套加锁场景。

十一、生产者消费者模型(工程最常用,手写代码题)

多线程版本

python 复制代码
import threading
from queue import Queue
import time

def producer(q:Queue):
    for i in range(5):
        q.put(i)
        print(f"生产:{i}")
        time.sleep(0.2)

def consumer(q:Queue):
    while True:
        item = q.get()
        print(f"消费:{item}")
        time.sleep(0.5)
        q.task_done()

if __name__ == "__main__":
    q = Queue(maxsize=3)
    t1 = threading.Thread(target=producer, args=(q,))
    t2 = threading.Thread(target=consumer, args=(q,))
    t2.daemon = True
    t1.start()
    t2.start()
    t1.join()
    q.join()
    print("全部任务完成")

面试考点:q.task_done() 和 q.join() 的配合。

十二、生产工程落地问题(业务侧)

  1. 多进程+GPU推理

重点!如果你批量跑图像推理

  • 每个子进程独立CUDA上下文;进程开多了显存占用成倍上涨
  • 推荐方案:单进程内多线程预处理 + 主进程GPU推理,不要开大量进程去创建CUDA上下文
  • 或者用进程池控制进程数量等于GPU卡数
  1. 异常捕获问题
  • 线程/进程内部异常,如果不捕获,会静默消失,主线程感知不到!
  • ThreadPoolExecutor 的future.result() 会把内部异常抛出来;手动Thread需要内部try-except。
  1. 优雅退出
  • 不要暴力kill;用Event做退出信号,通知子线程/子进程收尾、刷写文件
  • 守护线程不能用来做持久化写盘任务

十三、综合大题(手撕问答)

Q1:现在要写一个批量图片缺陷检测程序:读取图片(IO)+ 模型推理(CPU heavy),怎么设计并发架构?

参考答案:

  1. IO读取:多线程/asyncio 批量读取、解码图片
  2. 推理计算:进程池,分发图像张量到多个进程并行推理
  3. 中间用队列解耦:生产者(读图线程)→ 队列 → 消费者(推理进程池)
  4. 限制队列长度,防止图片堆积内存OOM

Q2:线上爬虫,目标网站限流,如何控制并发?

用 threading.Semaphore 信号量限制最大并发数,或者 ThreadPoolExecutor 的max_workers。

python 复制代码
# 信号量示例
import threading
sem = threading.Semaphore(3) # 最多3个并发

def fetch(url):
    with sem:
        print("请求", url)
        # http request

Q3:什么是孤儿进程、僵尸进程?

  • 僵尸进程:子进程执行结束,父进程没有调用wait回收退出状态,进程残留PCB,占用PID。
  • 孤儿进程:父进程提前退出,子进程被init进程收养,会自动回收。

Python multiprocessing,记得join或者及时回收,大量短任务容易堆积僵尸进程。

十四、思维导图式总结

  1. IO密集:优先 asyncio(高并发) / ThreadPoolExecutor(简单同步代码)
  2. CPU密集:ProcessPoolExecutor,避开GIL,利用多核
  3. 多线程:共享内存,注意锁、竞态;IO场景收益高
  4. 多进程:内存隔离,IPC成本高;适合图像、批量推理;小心pickle、显存拷贝
  5. 协程:单线程高并发IO,不能执行同步阻塞/CPU heavy代码

十五、延伸追问清单(继续深挖的问题)

  1. concurrent.futures 的 cancel() 能取消正在运行的任务吗?
  2. 信号量Semaphore和BoundedSemaphore区别?
  3. 进程间共享内存,多进程读写需要加锁吗?
  4. Celery 任务是多进程还是多线程?如何避免任务死锁?
  5. 多线程写同一个日志文件,怎么保证日志不混乱?
相关推荐
Data_Journal43 分钟前
如何使用 Python 解析 HTML
python·html
king13598745501 小时前
LiteRT Tensor API:C++ 编写端侧模型图,XNNPACK、GPU 与 NPU 怎么选?
开发语言·c++·人工智能·chatgpt·架构·aigc
geovindu1 小时前
rust: Template Method Pattern(续)
开发语言·后端·设计模式·rust·行为模式·板方法模式
外收内放1 小时前
面向对象高级(封装)
开发语言·python
全栈练习生1 小时前
大模型原理之投机解码
python·ai
青少儿编程课堂1 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:差分数组与区间修改实战解析
c++·python·算法·bfs·信息学竞赛
devilnumber1 小时前
Java 内部迭代 vs 外部迭代:从原理到实战避坑
java·开发语言
小小张说故事1 小时前
Python 的 sort 为什么又稳又快?拆开 Timsort:run 检测、minrun 与 galloping 的三板斧
python
学逆向的1 小时前
C printf原型解析
c语言·开发语言·网络安全