Python多线程编程常见问题怎么解决?,多线程死锁怎么解决
Python多线程编程的绝大多数问题,都绕不开GIL、共享变量竞争和线程池配置这三件事,把这三处理顺,剩下的坑基本是细节。下面这些内容来自我自己写爬虫、写任务调度脚本时踩过的坑,按“为什么会出问题—怎么改—怎么验证”的顺序讲。
GIL不是玄学:Python多线程到底在什么场景下才有意义
新手最常问的一句是“Python多线程是不是假的”。这个说法不算错,但也不完整。要判断它有没有用,得先看清楚GIL在管什么。
GIL到底锁住了什么
CPython解释器在执行字节码时,同一时刻只允许一个线程持有GIL。也就是说,纯Python的计算型代码,你开8个线程,本质上还是一个一个排队跑,切换还要额外开销。CPython官方文档对这个机制有明确说明,这不是实现缺陷,而是早期为简化内存管理和引用计数做的取舍。
但关键在于:GIL在遇到I/O阻塞时会主动释放。文件读写、网络请求、数据库查询、time.sleep,这些操作等待期间线程会让出GIL,其他线程就能接着干活。所以多线程真正的战场是I/O密集型任务,不是算力密集型任务。
什么时候多线程真能提速
- 01网络请求
批量调接口、抓页面,几十个线程并发,总耗时能压到原来的几分之一。
- 02文件与磁盘操作
批量读写小文件、图片转码前的读取阶段。
- 03等待型任务
轮询第三方状态、定时探测服务可用性。
- 04不适合
大矩阵运算、图像像素处理、加密解密这类纯CPU循环。
顺便提一句,Python 3.13开始提供了实验性的free-threaded构建,可以编译出不带GIL的解释器,但生态兼容性还在推进中。据Python官方文档的说明,这个模式目前不建议直接用于生产环境,短期内GIL仍然是默认行为。
Python多线程为什么比单线程还慢
这个问题我在一个日志清洗脚本上真实遇到过:改成20个线程后,跑得比原来慢了一倍多。原因不复杂,通常是下面几种叠加。
常见原因清单
- 任务本身是CPU密集型。线程切换加上抢GIL的开销,白忙一场。
- 线程数开太大。系统调度、上下文切换的成本会吃掉并发收益。I/O任务一般从8到32个线程起步试探,不是越多越好。
- 锁粒度过粗。一个全局锁把所有操作串起来,等于人为造了个单线程。
- 频繁创建销毁线程。每次new一个Thread都要走一遍系统调用,短任务尤其亏。
- 共享数据结构选错。用普通list做多线程队列,加锁逻辑写得不好就会反复等待。
验证方法很直接:用cProfile看耗时分布,或者在线程函数里打时间戳,统计每个线程真正执行的时间片占比。如果大量时间花在等待锁上,问题就在并发模型而不在代码效率。
Python多线程和多进程该怎么选
这是我在技术群里被问得最多的对比问题。判断标准其实只有一条:任务卡在等,还是卡在算。
| 维度 | 多线程(threading) | 多进程(multiprocessing) |
|---|---|---|
| 适用任务 | I/O密集:网络、文件、数据库 | CPU密集:计算、编解码、图像处理 |
| 内存占用 | 共享同一进程空间,占用小 | 每个进程独立内存,占用大 |
| 数据传递 | 直接读写,需加锁 | 需要队列或管道序列化 |
| 启动开销 | 小 | 较大,Windows下更明显 |
| 调试难度 | 竞争问题隐蔽,难复现 | 相对好定位 |
行业共识认为,超过七成的后台脚本场景属于I/O等待,先用线程池跑起来再说;只有当单机CPU占用稳定跑满、且数据可以分片时,才值得换成多进程或者干脆上异步。
threading模块的三个实操要点
理论说完了,落到代码上。这部分是我自己反复改过几版的写法。
要点一:用线程池,不要手动new
from concurrent.futures import ThreadPoolExecutor
def fetch(url):
# 这里放requests请求
return url
with ThreadPoolExecutor(max_workers=16) as pool:
results = list(pool.map(fetch, url_list))
ThreadPoolExecutor省掉了手动join和异常收集,map会按顺序返回结果。max_workers不要超过32,除非明确知道对方接口能扛住。
要点二:共享变量必须加锁,累加尤其危险
counter += 1看着是一行,实际是读取、加一、写回三步,线程一交错就丢数据。正确做法是threading.Lock()包住临界区,或者直接用queue.Queue把结果收回主线程统一处理。后者更快也更少出错,我个人更推荐。
要点三:异常不会自动冒出来
线程里抛的异常,默认不会中断主程序,也不会打印到你能看见的地方。用线程池时,异常会在取结果的那一刻才抛出;手动起线程时,记得在函数最外层套try/except并写日志,否则排查起来会怀疑人生。
Python多线程爬虫最容易翻车的地方
爬虫是多线程最典型的落地场景,也是最容易把对方服务器打挂、把自己IP封掉的场景。我的经验是三个约束必须提前定死。
并发数、限速、去重
- 01并发数
单站点建议不超过10,跨站点可以到30以上。
- 02限速
用time.sleep或者信号量控制每秒请求数,别裸奔。
- 03去重与任务队列
用queue.Queue存放待抓链接,配合一个已访问集合,避免线程重复抓同一页。
- 04会话复用
每个线程复用requests.Session,减少握手开销。
还有一个容易被忽略的点:线程安全地写文件。多个线程同时open同一个文件写日志,很容易出现内容错行。稳妥做法是让工作线程只往队列放数据,单独开一个写线程负责落盘。
出问题怎么查:三条可验证的排查路径
线程问题最烦的是“偶发”。下面几个手段我用得最多。
- 打线程名和时间戳。用
threading.current_thread().name配合time.time(),能看出是哪个线程卡住、卡了多久。 - 看线程栈。程序疑似挂死时,用
faulthandler.dump_traceback()或者py-spy dump --pid直接导出所有线程的调用栈,锁等待一眼就能看出来。 - 降并发复现。把线程数降到2,如果问题消失,基本可以锁定是竞争条件而不是逻辑错误。
据近年来一些线上事故复盘的经验看,多数“多线程导致数据错乱”的根因,落在共享可变对象没有隔离上,而不是Python本身的问题。
学习路径与一点现实观察
想系统补这块,我的建议是先读CPython官方文档的threading和concurrent.futures两章,再动手改写一个自己旧的串行脚本,比看视频有效得多。
顺带说两个现实情况:市面上的Python并发编程线上课程价格跨度很大,从几十元的录播到上千元的训练营都有,内容重合度不低,先看免费文档再决定要不要付费更划算。另外从招聘侧看,北京、杭州、深圳等地后端和数据开发岗位的JD里,“熟悉多线程与异步编程”几乎是标配要求,面试常考的就是GIL、线程池参数和锁的粒度这三类问题。
回到开头那句话:把GIL的适用边界、共享变量的隔离方式、线程池的数量控制搞清楚,Python多线程的坑其实就那么几处。
Python多线程编程常见问题Q&A
Q1:多线程里用print会不会有问题?
会有。多个线程同时print可能输出交错,尤其是长字符串。建议改成logging模块,它内部有锁,输出更规整,还能带线程名。
Q2:daemon线程和普通线程有什么区别?
设成daemon的线程会随主线程退出而强制结束,适合后台心跳、监控这类可随时中断的任务;普通线程则会阻塞主线程退出,直到执行完毕。数据落盘、事务提交这类工作不要放在daemon线程里,可能来不及完成。
Q3:Python多线程能利用多核CPU吗?
不能。受GIL限制,同一进程内的多个Python线程在任一时刻只有一个在执行字节码,多核无法被纯Python计算型代码同时占满。要吃到多核,需要用multiprocessing、concurrent.futures的ProcessPoolExecutor,或把计算下沉到NumPy这类释放GIL的C扩展里;仅就I/O等待场景而言,多线程依然能显著缩短总耗时。