Crispin's Blog

抢茅子时,Python 和 Go 的多线程到底差在哪?

茅子作为国酒,总有人写脚本抢购。Python 是大多数人的第一选择,但跑起来总感觉慢半拍。后面换成 Go 之后,明显快了。 所以很多人都说python是假多线程,这篇文章就来聊聊背后的原因。

问题的本质:抢购是 I/O 密集型任务

抢购脚本的核心逻辑大概是这样:

  1. 轮询库存接口,等待开售
  2. 一旦有货,立刻发起下单请求
  3. 并发发多个请求,提高成功率

整个过程 CPU 几乎不干活,大部分时间都在等网络响应。这是典型的 I/O 密集型场景。


Python 的 GIL:多线程的天花板

Python 有一个臭名昭著的东西叫 GIL(Global Interpreter Lock,全局解释器锁)

它的存在是为了保证 CPython 解释器在同一时刻只有一个线程在执行 Python 字节码,避免内存管理出问题。

1
2
3
4
5
6
7
8
9
10
11
12
import threading
import requests

def buy():
resp = requests.post("https://maotai.example.com/order", json={"sku": "53度飞天"})
print(resp.status_code)

threads = [threading.Thread(target=buy) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()

上面这段代码开了 10 个线程,看起来是并发的。但实际上:

  • 当一个线程在执行 Python 代码时,其他线程被 GIL 挡在门外
  • 只有当线程进入 I/O 等待(比如等网络响应)时,GIL 才会释放,让其他线程有机会运行

所以 Python 的多线程在 I/O 密集型场景下能用,但有额外开销:线程切换、GIL 争抢,都是负担。

asyncio:Python 的正确姿势

对于 I/O 密集型任务,Python 更推荐用 asyncio + aiohttp

1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
import aiohttp

async def buy(session):
async with session.post("https://maotai.example.com/order", json={"sku": "53度飞天"}) as resp:
print(resp.status)

async def main():
async with aiohttp.ClientSession() as session:
tasks = [buy(session) for _ in range(10)]
await asyncio.gather(*tasks)

asyncio.run(main())

asyncio 是单线程的事件循环,没有 GIL 争抢,并发效率更高。但它是协作式调度——你必须在代码里主动 await,否则整个循环会被卡住。


Go 的 goroutine:真正的并发

Go 没有 GIL。它的并发单元是 goroutine,由 Go 运行时调度,而不是操作系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"net/http"
"bytes"
"sync"
)

func buy(wg *sync.WaitGroup) {
defer wg.Done()
body := []byte(`{"sku":"53度飞天"}`)
resp, err := http.Post("https://maotai.example.com/order", "application/json", bytes.NewBuffer(body))
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(resp.StatusCode)
}

func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go buy(&wg)
}
wg.Wait()
}

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 是更合适的选择

当然,抢到茅台还是要靠运气,祝大家都能抢到心仪的茅子。

maotai