你之前写 Go,goroutine 加 channel 顺手就上了;换到 Python 想照抄这套,发现 GIL、asyncio、多进程各说各话,不知道该选哪个。这篇不重讲语法,给一条从任务类型出发的对照思路,配上落地时要定的超时、并发上限和排查顺序,让你能按手里的活儿直接挑方案。
你之前写 Go,goroutine 加 channel 顺手就上了;换到 Python 想照抄这套,发现 GIL、asyncio、多进程各说各话,不知道该选哪个。先给个结论:Python 没有 goroutine 的等价物,但每一层都有替代品,关键是按手里的任务类型挑,而不是找一个万能机制。
这篇不重讲语法,给你一条从任务类型出发的对照思路,配上落地时要定的超时、并发上限和排查顺序。看完你应该能直接判断手里的活儿该用线程、进程还是协程,而不是先写一遍再返工。
先对齐心智模型:Go 和 Python 差在哪
Go 的 goroutine 是运行时调度的轻量线程,channel 是语言级原语,调度器帮你把几万个并发单元摊到少量 OS 线程上。Python 没有这个统一的运行时层,你得自己在三套机制里挑:threading、multiprocessing、asyncio。它们各自解决不同层面的问题,不是互相替代的关系。
- GIL
- 全局解释器锁,同一时刻只允许一个线程执行 Python 字节码,纯计算任务拿不到并行
- 让出时机
- 线程在 IO 等待时释放 GIL,所以网络请求、文件读写这类场景多线程仍然有效
- 组合方式
- 调度靠 asyncio 事件循环,通信靠 queue 或 Channel 类库,隔离靠多进程,三层各管一段
按任务类型选替代方案:一张对照思路
下面这张表按任务特征给方向,不是硬性规定。你的实际场景可能跨两行,那就先按主要瓶颈选主机制,再补一层辅助。
| 任务特征 | 推荐机制 | 要留意的代价 |
|---|---|---|
| 大量网络请求、文件读写 | asyncio 配异步客户端 | 同步库混进来会卡死事件循环 |
| 少量 CPU 密集计算 | multiprocessing 或进程池 | 进程启动和数据序列化开销 |
| 混合负载 | 进程池跑协程,或队列分流 | 架构复杂度上升,调试更难 |
| 想随手起几千个并发单元 | 先确认是 IO 等待型再用协程 | 内存和调度成本会先顶上来 |
- 异步客户端优先选原生支持 asyncio 的库,别用同步驱动硬套线程池包装
- 进程池的任务参数和返回值必须可序列化,传不了的对象要提前拆成基础类型
- 混合负载里,CPU 活丢给进程、IO 活留在事件循环,别让一个进程既算又等
- 并发数不是越高越好,先测出下游能接住多少,再定上限
落地时最容易踩的坑
大部分「Python 并发不给力」的抱怨,最后都指向同一个原因:异步函数里混进了同步阻塞调用。这会让整个事件循环卡住,表现是并发数上不去、延迟一起涨,看起来像性能问题,其实是写法问题。
- 排查事件循环阻塞在协程里临时加日志,看有没有某段代码执行时间明显偏长;同步数据库驱动和 requests 是最常见的元凶,换成异步版本或丢到线程池跑
- 确认并发瓶颈在哪并发一提就报限流错误,说明瓶颈在下游接口而不是你的代码;去 /guides/429-concurrency-backoff 看排队与退避的分档做法,别硬提并发
- 分开设超时连接超时、读取超时、整体超时要分开定,参考 /guides/api-timeout-tuning 的分档清单,否则排查时分不清是连不上还是算得慢
还有一个和 Go 差异最大的地方:多进程之间共享状态要靠队列或外部存储,直接改全局变量不会生效。如果你习惯 Go 的共享内存模型,这里会栽跟头。
上线前要跑一遍的检查清单
选完方案不算完,上线前这几步能帮你少走一轮返工。重点是先有基线数字,再看变化,别拿没测过的感觉当依据。
- 先量基线:用 /tools/speed-test 测出单请求延迟,再压并发,没有基线就不知道优化有没有效果
- 压测时同时观察并发数、错误率、P99 延迟,只看平均值会把长尾问题藏起来
- 限流和重试要配对设计:什么错误重试、重试几次、退避怎么涨,别让重试把下游打得更死
- 日志里记下任务类型、并发档位、失败原因,下次调参才有参照
这套清单对 IO 密集型任务最有效。如果你的任务里 CPU 计算占大头,重点应该放在进程数和数据切分上,而不是压并发连接数。
什么时候这套对比不适用
不是所有场景都值得纠结并发模型。以下几种情况,换机制带来的收益可能还不如把同步逻辑写清楚。
- 瓶颈在模型接口的响应速度和额度限制,不在语言层:先去 /sites 和 /rankings 看服务本身的稳定性,再谈并发优化
- 延迟敏感、单请求耗时很短的场景:多进程启动开销可能比省下的时间还多,先用同步写法
- 团队没人维护过异步代码:硬上 asyncio 的长期维护成本可能高于直接用线程池,选型要把人力算进去
判断标准很简单:如果换方案后,你没法用一句话说清「它到底省掉了哪一段等待或计算」,那这个改动大概率不值得做。价格和稳定性这类服务侧数据,去 /price-watch 核对当时的数字,别拿记忆里的值做决策。
下一步把你手里最慢的那个任务拿出来,先按任务类型对一遍上面的对照表,再跑一次基线压测;服务侧的价格和稳定性数据到 /price-watch 和 /sites 核对当时的数字。→常见问题
Python 里有没有跟 goroutine 完全对等的东西?
没有。goroutine 是语言运行时层面的轻量并发单元,Python 侧最接近的是 asyncio 协程,但它是单线程事件循环,CPU 密集任务并不能因此并行。要真并行只能上多进程,或者说换用别的运行时。
IO 密集任务用 asyncio 还是多线程?
两者都能让 IO 等待期间去干别的活。区别在数量级和写法:几百个并发连接多线程够用,几千级连接 asyncio 更省资源,但要求整条链路都是异步库,混进一个同步调用就会拖慢整个循环。
CPU 密集任务怎么处理才不掉进 GIL 的坑?
用多进程而非多线程,把计算拆成独立任务提交给进程池。要注意进程间传数据要序列化,大对象传输开销不小;如果单个任务本身就很短,进程开销可能盖过收益,这时考虑换 C 扩展或外部计算服务。