3、Python并发基石:GIL锁的真相、多线程 vs 多进程的选择、协程的引入
聊到Python并发,有个绕不开的话题——GIL。很多新手一上来就问我:“Python多线程是不是假的?” 嗯,这个问题背后,其实藏着对GIL深深的误解。今天我就带大家把这层窗户纸捅破。
3.1 GIL锁:它到底是什么?
GIL,全称Global Interpreter Lock,全局解释器锁。说白了,就是CPython解释器里的一把大锁。它规定:同一时刻,只有一个线程能执行Python字节码。
为什么会这样?这得从Python的内存管理说起。Python的对象引用计数是线程不安全的。如果没有GIL,两个线程同时修改一个对象的引用计数,内存就乱了。我早期做C扩展时,就踩过这个坑——手动管理引用计数,稍不留神就段错误。
核心结论:GIL保护的是Python解释器内部状态,不是你的业务逻辑。它让单线程代码跑得更快,但限制了多线程的并行能力。
3.2 GIL的真相:它真的那么糟糕吗?
其实,GIL对I/O密集型任务影响不大。为什么?因为I/O操作会释放GIL。比如网络请求、文件读写,线程在等待I/O时,GIL会被释放,其他线程就能趁机执行。
我做过一个爬虫项目,用多线程抓取1000个网页。单线程要跑2分钟,多线程(8个线程)只要30秒。你看,GIL并没有成为瓶颈。
但CPU密集型任务就惨了。比如图像处理、数值计算,每个线程都需要大量CPU时间。GIL会让它们轮流执行,实际上变成了单核效果。我曾经用多线程做矩阵乘法,结果比单线程还慢——因为线程切换有开销。
| 任务类型 | 多线程效果 | 原因 |
|---|---|---|
| I/O密集型 | 显著提升 | I/O等待时释放GIL |
| CPU密集型 | 无提升甚至下降 | GIL限制并行执行 |
| 混合型 | 部分提升 | 取决于I/O占比 |
避坑指南:我曾经在数据处理任务中用了多线程,结果发现CPU利用率只有100%(单核)。后来换成多进程,CPU直接飙到800%。记住:CPU密集型用多进程,I/O密集型用多线程。
3.3 多线程 vs 多进程:怎么选?
这个问题,我每次面试都会问。很多人背过答案,但真正遇到场景就懵了。
多线程(threading):
- 共享内存,数据传递方便
- 创建开销小,适合大量并发连接
- 受GIL限制,不适合CPU密集型
- 典型场景:Web服务器、爬虫、GUI应用
多进程(multiprocessing):
- 每个进程有独立GIL,真正并行
- 内存隔离,数据传递需要序列化
- 创建开销大,进程数不宜过多
- 典型场景:图像处理、科学计算、机器学习
我个人的习惯是:先问自己“瓶颈在CPU还是I/O?” 如果答案是I/O,用多线程+协程;如果是CPU,用多进程。如果两者都有,那就混合使用——多进程处理计算,每个进程内用多线程处理I/O。
注意:多进程不是越多越好。进程数超过CPU核心数,反而会因为上下文切换降低性能。一般设置为CPU核心数或核心数+1。
3.4 协程的引入:轻量级并发方案
协程,说白了就是用户态线程。它不依赖操作系统调度,而是由程序自己控制。Python的协程通过async/await实现,底层是事件循环。
为什么需要协程?你想想看,多线程有GIL限制,多进程又太重。对于I/O密集型任务,协程是最优雅的方案——单线程内就能实现高并发。
我做过一个WebSocket服务,需要同时处理10万+连接。如果用多线程,每个线程至少8KB栈空间,10万线程就是800MB内存,还不算切换开销。换成协程,每个协程只占几百字节,10万协程轻松搞定。
import asyncio
async def fetch_data(url):
# 模拟网络请求
await asyncio.sleep(1)
return f"Data from {url}"
async def main():
tasks = [fetch_data(f"http://example.com/{i}") for i in range(100)]
results = await asyncio.gather(*tasks)
print(f"Fetched {len(results)} items")
asyncio.run(main())
这段代码启动了100个协程,每个协程模拟1秒的网络请求。总耗时只有1秒左右——因为它们在等待时都让出了控制权。
3.5 三者的选择策略
嗯,这里我总结一下选择策略:
- I/O密集型 + 高并发连接:首选协程(asyncio)。如果协程生态不完善,退而求其次用多线程。
- CPU密集型 + 计算量大:多进程(multiprocessing)。注意进程间通信开销。
- 混合型任务:多进程 + 协程。进程处理计算,进程内协程处理I/O。
- 简单脚本/小工具:单线程就够了,别为了并发而并发。
我的经验:不要一开始就上并发。先写单线程版本,用性能分析工具找出瓶颈。很多时候,优化算法比引入并发更有效。我曾经优化过一个排序任务,把冒泡排序换成快速排序,性能提升了100倍——比任何并发方案都管用。
3.6 知识体系总览
下面这张图,是我梳理的Python并发知识体系。你可以把它当作一张地图,随时回来查阅。
这张图把今天讲的内容串起来了。你可以看到,GIL是理解多线程的关键,而协程则提供了另一种思路。三者各有适用场景,没有银弹。
最后说一句:并发不是目的,提升性能才是。别为了用并发而用并发,先想清楚你的瓶颈在哪。
公众号:蓝海资料掘金营,微信deep3321