我做了三年Python开发,刚开始写多线程的时候,跟很多人一样兴奋。网上都说Python多线程能提升性能,我就兴冲冲地在一个爬虫项目里用了。结果跑起来一看,CPU占用率才50%,两个线程加起来比单线程还慢。这就离谱了。
问题出在哪里?核心就在GIL这个东西上。GIL全称是全局解释器锁,你可以把它理解成一把大门钥匙。Python解释器规定,任何线程想执行代码,必须先拿到这把钥匙。同一时间只有一个线程能拿到钥匙。你的线程再多,一次也只能进去一个人干活。这就好比你去食堂打饭,窗口只有一个,排队的人再多也没用。
很多人问,那多线程是不是就完全没用?也不对。你要分情况。如果你的程序是IO密集型的,比如网络请求、文件读写、数据库查询,多线程能帮你省时间。线程在等网络响应的时候,会把钥匙交出来,让别的线程去干活。你爬100个网页,单线程得一个个等,多线程可以同时等,总时间就短了。
但如果你是做CPU密集型的计算,比如图像处理、数据分析、复杂运算,多线程就帮倒忙了。每个线程都要抢钥匙,抢来抢去还要切换上下文,额外开销特别大。我试过把一段纯数值计算写成多线程,结果运行时间从5秒变成了8秒。这不是写了等于没写,这是写了还不如不写。
那CPU密集型任务怎么搞?换个思路。Python官方推荐用multiprocessing模块。它创建的是独立进程,每个进程有自己的GIL,互不影响。你可以把任务拆成八份,扔给八个进程跑,CPU能直接用到八个核心。需要注意进程间通信要靠队列或管道,不能直接共享变量,这点写起来比多线程麻烦。
还有高手会用C扩展。比如用Cython或者C语言写一个计算模块,在模块里释放GIL。这样Python的线程调用C函数时,锁就解开了,多个线程能同时跑。但门槛高,调试起来更费劲。
说回多线程本身。很多人用不好GIL,是因为没想明白一个道理:多线程是给“等待”提效的,不是给“计算”提效的。你花时间在等,就开线程。你花时间在算,就开进程。这个道理我花了两年才悟透,中间踩了无数坑。
有一次我把一个统计系统从单线程改成多线程,数据库查询那种,性能确实上去了。后来同组同事照猫画虎,把一个图片压缩工具也改成了多线程,结果CPU爆满,内存占用几百兆,最后被运维找上门。这就是没搞明白自己的场景。
我现在写Python并发,先看类型。是IO型就上ThreadPoolExecutor,设个合适的线程数,比如100个,省心。是CPU型就用ProcessPoolExecutor,或者直接上async。要是非得又等又算,那得小心拆分,别让GIL卡住。
最后说句实在话,GIL不是bug,是设计选择。它保证了Python的内存安全,让你不用操心死锁、数据竞争这些头疼问题。你想享受单线程开发的简单安全,就得接受它的局限性。别跟GIL较劲,你改变不了它。你要做的是找到适合自己的那把钥匙。