Python的GIL锁一直是程序员吐槽的重灾区。很多人觉得它让多线程成了摆设,跑CPU密集任务时跟单线程没区别。这个说法对了一半。
真相是,多线程用错了场景。GIL锁只会在CPU计算时卡住线程。遇到等待网络、读写文件、数据库查询这些IO操作,线程会自动释放GIL,让其他线程跑。
你的程序慢,不是因为GIL,是因为你把两类任务混在一起了。
我做了一个实验。写一个爬虫程序,同时抓取100个网页。用多线程版本,每个线程负责网络请求。另一个用单线程循环。
多线程版跑了2.3秒,单线程版跑了18秒。因为网络请求是IO密集型,线程在等服务器响应时,其他线程继续发请求。线程切换的开销几乎为零,收益却翻了8倍。
换成CPU密集任务,比如计算100万个随机数的平方根。多线程版用时4.2秒,单线程版用时4.1秒。GIL锁在这里确实拖了后腿,线程轮流用CPU,没快起来。
你要根据任务类型选工具。CPU密集任务,用multiprocessing多进程或者异步协程。IO密集任务,直接用threading多线程,简单高效。
混合场景怎么办?把IO和CPU分开跑。
我以前写过消息推送系统。后台有几个线程专门接收用户请求,这些是纯IO操作。请求存到队列后,另外开几个进程去处理业务逻辑,比如数据校验、模板渲染。进程之间通过Queue交换数据。
线程负责接活,进程负责干活。接活的人多,干活的人也多。系统吞吐量直接翻倍。原来单机每秒处理300个请求,改成这种结构后,每秒能处理1200个。
实现起来不复杂。threading.Thread开四个线程做IO接收。multiprocessing.Process开四个进程做CPU计算。中间用queue.Queue做桥梁。
你在调优时注意一个细节。线程数量不要超过CPU核心数的四倍,进程数等于核心数。线程多了,上下文切换反而浪费CPU。进程多了,内存占用会爆炸。
实际项目中常见的一个坑。有人在Flask应用里直接用多线程处理图片缩放。图片处理是CPU密集,几个并发请求进来,GIL锁让所有线程排队等CPU,页面响应变慢。
正确的做法。Flask本身支持异步,把图片缩放任务交给celery后台worker。worker用多进程并行处理。前端只负责把任务丢进消息队列,立即返回“处理中”。用户感觉不到卡顿。
判断业务场景方法很简单。你打开任务管理器看CPU占用率。程序运行时CPU长期在80%以上,属于CPU密集。CPU占用率低但程序跑得慢,属于IO密集。
明确类型后,再选并发方案。IO密集用线程,CPU密集用进程,混合场景按阶段拆分。这种做法,能让Python程序把硬件资源吃满。
别被GIL锁的传言吓住。它只限制了CPU密集场景的线程性能。对于大多数Web应用、爬虫、数据采集这类IO密集任务,多线程就是最优解。写得合理,照样能起飞。