1. 为什么Python需要多进程与多线程?
当你用Python处理一个需要重复执行的任务时,比如批量处理图片或者爬取网页数据,单线程程序就像一个人在厨房里既切菜又煮汤——效率低下且浪费时间。这时候多线程和多进程就像请来了帮手,可以同时处理多个任务。
但Python有个特殊限制:全局解释器锁(GIL)。这就像厨房里只有一个灶台,即使有多个厨师(线程),同一时间也只能有一个人在炒菜。不过别担心,多进程可以绕过这个限制,因为每个进程都有自己的灶台(CPU核心)。
我做过一个实验:用单线程处理100张图片需要120秒,而用4个进程只需要35秒。这就是为什么理解多进程和多线程如此重要。
2. 如何判断任务类型?
2.1 CPU密集型任务
这类任务主要消耗CPU资源,比如:
- 数学计算(如机器学习模型训练)
- 图像处理
- 数据加密/解密
判断技巧:如果你的代码大部分时间都在做数学运算,几乎没有文件读写或网络请求,那就是CPU密集型。
我曾经优化过一个图像处理脚本,发现90%的时间都花在了像素计算上。这时候增加线程数反而让程序更慢,因为线程切换带来了额外开销。
2.2 IO密集型任务
这类任务大部分时间在等待外部响应,比如:
- 网络请求(爬虫、API调用)
- 文件读写
- 数据库查询
有个简单的方法可以判断:在Linux下运行top命令,如果CPU使用率很低但程序运行很慢,很可能就是IO密集型。
实际案例:我开发过一个爬虫,单线程每小时只能爬取200个页面。改成多线程后(50个线程),性能提升了15倍,而CPU使用率只增加了20%。
3. 多进程实战:充分利用CPU核心
3.1 获取CPU核心数
import multiprocessing cpu_count = multiprocessing.cpu_count() print(f"你的CPU有{cpu_count}个逻辑核心")注意:超线程技术会让CPU核心数显示为物理核心的2倍。对于计算密集型任务,建议使用物理核心数。
3.2 进程池的最佳实践
from multiprocessing import Pool import time def cpu_bound_task(n): return sum(i*i for i in range(n)) if __name__ == '__main__': start = time.time() with Pool(processes=cpu_count) as pool: results = pool.map(cpu_bound_task, [1000000]*8) print(f"耗时:{time.time()-start:.2f}秒")在我的6核CPU上测试:
- 进程数=6:耗时4.2秒
- 进程数=12:耗时4.5秒(反而更慢)
- 进程数=3:耗时6.8秒
结论:对于纯计算任务,进程数等于物理核心数时效率最高。
4. 多线程调优:突破IO瓶颈
4.1 线程数计算公式
对于IO密集型任务,一个经验公式:
最佳线程数 = (IO等待时间 + CPU处理时间) / CPU处理时间 * CPU核心数举例:一个网络请求任务:
- 每次请求CPU处理时间:5ms
- 网络等待时间:95ms
- CPU核心数:4
计算:(95+5)/5 * 4 = 80个线程
4.2 线程池示例
from concurrent.futures import ThreadPoolExecutor import requests def fetch_url(url): response = requests.get(url) return len(response.text) urls = ["https://example.com"]*100 with ThreadPoolExecutor(max_workers=80) as executor: results = list(executor.map(fetch_url, urls))在我的测试中,线程数从10增加到80时,完成时间从18秒降到2.3秒。超过100后改善不明显,反而可能因内存不足导致性能下降。
5. 混合使用多进程和多线程
对于既有CPU计算又有IO等待的任务,可以这样组合:
from multiprocessing import Pool from concurrent.futures import ThreadPoolExecutor import numpy as np def process_data(data): # CPU密集型计算 result = np.mean(data)**2 return result def io_task(url): # IO密集型任务 data = download_data(url) with ThreadPoolExecutor(4) as executor: processed = list(executor.map(process_data, data)) return processed if __name__ == '__main__': urls = [...] # 100个数据源URL with Pool(processes=6) as pool: all_results = pool.map(io_task, urls)这种架构下:
- 外层用多进程绕过GIL限制
- 内层用多线程处理IO等待
- 每个进程内的线程数建议为CPU核心数的1-2倍
6. 常见误区与性能陷阱
盲目增加线程数:我见过有人设置1000个线程,结果导致系统崩溃。线程切换开销会抵消并发优势。
忽略资源共享问题:多进程间共享数据需要使用Queue或共享内存,直接共享变量会导致数据不一致。
错误使用全局变量:多线程修改全局变量需要使用锁,否则会出现竞态条件。
忘记关闭资源:线程池/进程池如果不正确关闭会导致资源泄漏。一定要使用
with语句或手动调用shutdown()。
一个真实案例:某次我用多进程处理数据时忘记关闭数据库连接,导致达到最大连接数限制,整个服务不可用。后来改用连接池才解决问题。
7. 高级技巧:动态调整并发数
对于不确定任务类型的情况,可以实现动态调节:
import time from threading import active_count from concurrent.futures import ThreadPoolExecutor class DynamicThreadPool: def __init__(self, max_workers): self.executor = ThreadPoolExecutor(max_workers) self.max_workers = max_workers self.adjust_interval = 5 def monitor(self): while True: active = active_count() if active > self.max_workers * 0.8: # 考虑增加线程数 pass time.sleep(self.adjust_interval)这种模式适合任务类型变化大的场景,比如白天处理用户请求(IO密集),夜间跑批处理任务(CPU密集)。
8. 实际项目经验分享
去年优化一个电商价格计算服务时,我经历了这样的调优过程:
- 最初版本:单进程单线程,处理10万商品需要45分钟
- 第一版优化:改用多线程(20线程),时间降到15分钟
- 发现问题:CPU使用率只有30%,因为涉及大量数据库查询
- 最终方案:4个进程,每个进程内10个线程,最终耗时4分钟
关键发现:价格计算本身是CPU密集型,但需要先查询数据库(IO密集)。混合方案取得了最佳效果。
监控工具推荐:
psutil监控CPU/内存使用率time.perf_counter()测量代码段执行时间threading.enumerate()查看线程状态