抢茅子时,Python 和 Go 的多线程到底差在哪?
茅子作为国酒,总有人写脚本抢购。Python 是大多数人的第一选择,但跑起来总感觉慢半拍。后面换成 Go 之后,明显快了。 所以很多人都说python是假多线程,这篇文章就来聊聊背后的原因。
问题的本质:抢购是 I/O 密集型任务
抢购脚本的核心逻辑大概是这样:
- 轮询库存接口,等待开售
- 一旦有货,立刻发起下单请求
- 并发发多个请求,提高成功率
整个过程 CPU 几乎不干活,大部分时间都在等网络响应。这是典型的 I/O 密集型场景。
Python 的 GIL:多线程的天花板
Python 有一个臭名昭著的东西叫 GIL(Global Interpreter Lock,全局解释器锁)。
它的存在是为了保证 CPython 解释器在同一时刻只有一个线程在执行 Python 字节码,避免内存管理出问题。
1 | import threading |
上面这段代码开了 10 个线程,看起来是并发的。但实际上:
- 当一个线程在执行 Python 代码时,其他线程被 GIL 挡在门外
- 只有当线程进入 I/O 等待(比如等网络响应)时,GIL 才会释放,让其他线程有机会运行
所以 Python 的多线程在 I/O 密集型场景下能用,但有额外开销:线程切换、GIL 争抢,都是负担。
asyncio:Python 的正确姿势
对于 I/O 密集型任务,Python 更推荐用 asyncio + aiohttp:
1 | import asyncio |
asyncio 是单线程的事件循环,没有 GIL 争抢,并发效率更高。但它是协作式调度——你必须在代码里主动 await,否则整个循环会被卡住。
Go 的 goroutine:真正的并发
Go 没有 GIL。它的并发单元是 goroutine,由 Go 运行时调度,而不是操作系统。
1 | package main |
goroutine 的特点:
- 极轻量:初始栈只有 2KB,可以轻松开几万个
- 抢占式调度:Go 运行时会主动调度 goroutine,不需要你手动
await - 真正并行:在多核机器上,多个 goroutine 可以同时跑在不同的 CPU 核心上
直接对比
| Python 多线程 | Python asyncio | Go goroutine | |
|---|---|---|---|
| 有无 GIL | 有 | 有(单线程绕开) | 无 |
| 调度方式 | OS 线程 + GIL | 事件循环(协作式) | Go runtime(抢占式) |
| 内存开销 | ~1MB/线程 | 极低 | ~2KB/goroutine |
| 并发数上限 | 数百 | 数万 | 数十万 |
| 代码复杂度 | 低 | 中(需要 async/await) | 低 |
| I/O 密集型性能 | 一般 | 好 | 好 |
抢购场景下的实际差距
假设你要并发发 100 个请求,每个请求耗时 200ms:
- Python 多线程:线程切换开销 + GIL 争抢,实际并发度受限,总耗时可能在 400~600ms
- Python asyncio:单线程事件循环,理论上接近 200ms,但受限于事件循环本身的调度延迟
- Go goroutine:100 个 goroutine 真正并发,总耗时接近单次请求的 200ms
在抢购这种毫秒必争的场景下,这个差距是实质性的。
结论
- 如果你用 Python 写抢购脚本,用 asyncio + aiohttp,不要用多线程
- 如果你追求极致并发和低延迟,Go 是更合适的选择
当然,抢到茅台还是要靠运气,祝大家都能抢到心仪的茅子。