Go 和 Python 的并发模型对比:进程、线程、协程、并发和并行到底怎么理解?
最近我在写 worker 任务系统的时候,重新理解了一遍 Python 和 Go 的并发差异。
以前写 Python,多 worker 经常要考虑:
多进程怎么管理?
日志会不会串?
一个 worker 崩了怎么办?
怎么吃满多核心?
后来换成 Go,发现一个进程里开多个 goroutine worker 就很自然:
go worker(1)
go worker(2)
go worker(3)
go worker(4)
日志也好管,状态也好管,而且单进程还能利用多个 CPU 核心。
一开始很容易误会成:
Python 不行,Go 行
但更准确的理解应该是:
Python 和 Go 的并发模型不一样。
Python 常见靠多进程吃多核心。
Go 常见靠单进程多 goroutine 吃多核心。
这篇文章就把这个事情从进程、线程、协程、GIL、goroutine、并发和并行几个角度捋一遍。
一、先分清:进程、线程、协程、goroutine
先看几个基本概念。
1. 进程
进程是操作系统分配资源的单位。
一个进程有自己的:
内存空间
文件句柄
网络连接
环境变量
日志句柄
运行状态
比如你启动一个 Python 服务:
python app.py
这就是一个进程。
你启动一个 Go 服务:
./app
这也是一个进程。
进程之间默认是隔离的。
一个进程里的内存,另一个进程不能直接访问。
所以多进程的好处是隔离性强,坏处是共享状态麻烦。
2. 线程
线程是 CPU 调度执行的基本单位。
一个进程里面可以有多个线程。
可以粗略理解成:
进程 = 一个公司
线程 = 公司里的员工
公司拥有资源,员工负责干活。
多个线程共享同一个进程里的内存空间,所以线程之间共享数据比进程容易。
但也因为共享内存,所以要考虑锁、并发安全、数据竞争。
3. 协程
协程是更轻量的任务。
它通常不是由操作系统直接调度,而是由语言 runtime 或事件循环调度。
协程出现的核心原因是:
线程太重,但程序里有大量等待。
后端服务里大量时间不是在计算,而是在等:
等数据库
等 Redis
等 HTTP 接口
等文件读写
等第三方 API
等模型服务返回
等待期间 CPU 其实没干活。
如果每一个等待任务都占一个线程,资源就很浪费。
协程的核心思想是:
遇到等待时,主动让出执行权,让别的任务先跑。
比如 Python 里的:
async def task():
result = await call_api()
return result
执行到 await call_api() 时,这个协程会说:
我现在要等接口返回。
这段时间我先让出执行权。
你去执行别的协程。
等接口返回了,再回来继续执行我。
所以协程非常适合 I/O 密集型任务。
4. goroutine
goroutine 是 Go 语言里的轻量并发任务。
它可以看成 Go 版的协程,但比 Python asyncio 的协程更强一些。
Go 里开一个 goroutine 很简单:
go worker()
背后由 Go runtime 调度。
Go runtime 会把大量 goroutine 调度到底层多个 OS 线程上,再由操作系统把这些线程分配到多个 CPU 核心上执行。
大概是:
多个 goroutine
↓
Go runtime 调度
↓
多个 OS thread
↓
多个 CPU core
所以 Go 的 goroutine 既轻量,又可以利用多核心。
这是 Go 写 worker、高并发服务非常舒服的原因之一。
二、并发和并行不是一回事
这两个词非常容易混。
并发 concurrency
并发的意思是:
多个任务都在推进,时间上重叠。
但它们不一定真的在同一瞬间一起执行。
比如一个人同时处理很多外卖订单:
A 单已经下了,等商家做
B 单已经下了,等骑手取
C 单已经下了,等支付回调
这个人不是同时干三件事,而是在多个任务之间切换。
但从整体看,多个任务都在推进。
这就是并发。
并行 parallelism
并行的意思是:
多个任务真的在同一时刻执行。
比如有 4 个 CPU 核心:
核心 1 跑任务 A
核心 2 跑任务 B
核心 3 跑任务 C
核心 4 跑任务 D
这才叫并行。
一句话区分
可以这么记:
并发:一个人同时管理很多事。
并行:很多人同时干很多事。
或者:
并发解决的是任务调度问题。
并行解决的是多核心同时计算问题。
Python asyncio 主要解决并发。
Go goroutine 既能做并发,也可以配合 runtime 做多核心并行。
三、Python 的 GIL 是什么?
这里说的 Python,主要指最常见的 CPython。
CPython 有一个很重要的机制:GIL。
GIL 全称是:
Global Interpreter Lock
全局解释器锁
它的核心影响是:
同一个 Python 进程里,同一时刻通常只有一个线程能执行 Python 字节码。
所以,如果你写:
import threading
for i in range(4):
threading.Thread(target=worker).start()
看起来开了 4 个线程。
但如果 worker 里做的是纯 Python CPU 计算:
def worker():
x = 0
for i in range(100_000_000):
x += i
那么这 4 个线程通常不能真正同时吃满 4 个 CPU 核心。
它更像是:
线程 1 执行一会儿
线程 2 执行一会儿
线程 3 执行一会儿
线程 4 执行一会儿
它们在切换,但同一时刻主要还是一个线程在执行 Python 字节码。
所以 CPU 密集型场景下,Python 单进程多线程通常提升不明显。
可以粗略记成:
CPython 单进程 + 多线程 + 纯 Python CPU 计算
≈ 主要有效使用一个 CPU 核心
四、Python 多线程是不是没用?
不是。
Python 多线程在 I/O 密集型任务里仍然有用。
比如:
请求接口
等待数据库
读写文件
等待 Redis
等待网络返回
这些等待期间,线程可以让出 GIL。
所以 Python 多线程适合:
I/O 密集型任务
不太适合:
纯 Python CPU 密集型任务
这是关键区别。
五、Python 协程 asyncio 是什么模型?
Python 的 asyncio 协程,通常是:
一个事件循环 event loop
绑定一个线程
这个线程里调度很多协程
大概是:
一个线程
├── 协程 A:跑到 await,开始等网络
├── 协程 B:接着跑,跑到 await,开始等数据库
├── 协程 C:接着跑,跑到 await,开始等 Redis
└── 协程 A:网络结果回来了,继续跑
也就是说,Python asyncio 的协程并发,本质上通常是:
一个线程里调度很多协程
它不是:
协程 A 跑在 CPU core 1
协程 B 跑在 CPU core 2
协程 C 跑在 CPU core 3
它更像:
一个线程在很多协程之间快速切换。
所以 Python asyncio 主要解决的是并发,不是多核心并行。
六、Python 协程为什么能高并发?
因为大量任务都在等待。
比如:
async def fetch(i):
print("start", i)
await asyncio.sleep(1)
print("done", i)
如果启动 100 个这样的协程,它们不是每个都占一个线程。
它们会在 await 的时候让出执行权。
所以很多等待可以重叠起来。
这就是协程的价值:
用少量线程管理大量 I/O 等待任务。
但是如果协程里写 CPU 死循环:
async def bad_task():
while True:
pass
那就麻烦了。
因为它没有 await,不会主动让出执行权。
整个事件循环可能会被它卡死,其他协程都没机会执行。
所以 Python 协程适合:
网络 I/O
数据库 I/O
Redis I/O
第三方 API
WebSocket
SSE 长连接
爬虫
不适合直接解决:
CPU 密集型计算
视频编码
图像处理
本地模型推理
复杂数学计算
七、Python 想吃多核心怎么办?
常见方式是多进程。
比如:
from multiprocessing import Process
for i in range(4):
Process(target=worker).start()
这时候是:
4 个 Python 进程
每个进程有自己的 GIL
于是就可以真正利用多个 CPU 核心。
很多 Python 服务也是这个部署思路:
uvicorn app:app --workers 4
或者:
gunicorn -w 4 app:app
这本质上就是开 4 个进程。
每个进程可以有自己的事件循环、自己的线程、自己的连接池、自己的日志句柄。
所以 Python 里常见模型是:
多进程吃多核心
每个进程内部再用线程或协程处理 I/O
例如:
process 1 -> event loop -> many coroutines
process 2 -> event loop -> many coroutines
process 3 -> event loop -> many coroutines
process 4 -> event loop -> many coroutines
八、Python 多进程的代价
Python 多进程能吃多核心,但代价也明显。
每个进程都有自己的:
内存空间
logger
文件句柄
数据库连接池
Redis 连接池
缓存
任务状态
所以会带来一些复杂度。
比如日志。
如果 4 个 Python 进程都写同一个 app.log:
worker1 写 app.log
worker2 写 app.log
worker3 写 app.log
worker4 写 app.log
可能会遇到:
日志交错
日志切割冲突
文件句柄不一致
一个进程写旧文件,另一个进程写新文件
多个进程同时 rename 日志文件
尤其是按天切日志时:
2026-04-28.log
2026-04-29.log
到了零点,多个进程都可能判断“该切日志了”。
这时候就很容易乱。
所以 Python 多进程里常见做法是:
每个 worker 写自己的日志文件
比如:
worker_1.log
worker_2.log
worker_3.log
worker_4.log
或者用更成熟的方式:
多个 worker 产生日志
↓
日志队列
↓
单独 logging listener 统一落盘
本质就是:
多个进程不要直接抢同一个日志文件。
九、Go 的 goroutine 为什么爽?
Go 的常见并发模型是:
一个进程
里面开很多 goroutine
比如:
for i := 0; i < 4; i++ {
go worker(i)
}
这不是 4 个进程。
这是一个 Go 进程里的 4 个 goroutine。
它们共享:
同一个进程空间
同一个 logger
同一个配置
同一个数据库连接池
同一个 Redis 连接池
同一个任务状态管理
所以统一管理很舒服。
更关键的是,Go 单进程不等于单核心。
Go runtime 会把 goroutine 调度到底层多个 OS 线程上,多个线程再被系统分配到多个 CPU 核心上。
可以理解成:
一个 Go 进程
├── goroutine 1 -> thread 1 -> core 1
├── goroutine 2 -> thread 2 -> core 2
├── goroutine 3 -> thread 3 -> core 3
└── goroutine 4 -> thread 4 -> core 4
所以:
Go 单进程 ≠ 单核心
Go 单进程可以利用多个 CPU 核心
这个能力由 GOMAXPROCS 控制。
现在 Go 默认会根据 CPU 核心数设置,一般不用手动改。
十、Go 的日志为什么更好管?
因为 Go 常见是单进程多 goroutine。
同一个进程里的 goroutine 共享同一个 logger。
Go 标准库里的 logger 通常会做锁保护。
多个 goroutine 写同一个 logger 时,会串行化写入。
大概是:
goroutine 1 要写日志 -> 加锁 -> 写完 -> 解锁
goroutine 2 要写日志 -> 等待 -> 加锁 -> 写完 -> 解锁
所以在一个 Go 进程里,多个 goroutine worker 写同一个日志文件通常是合理的。
这和多个进程同时写同一个文件不一样。
多个 goroutine 是同一进程内部的并发。
多个进程是操作系统层面的多个独立进程。
所以更准确地说:
Go 单进程多 goroutine 写日志好管
不是因为 Go 的文件系统有魔法
而是因为它们在同一个进程里,能共享同一个 logger 和锁
如果你启动多个 Go 进程,或者多个 Docker 容器副本,同时写同一个日志文件,也一样会变复杂。
十一、Go 和 Python 的并发模型对比
可以简单对比一下。
Python 单进程多线程
适合 I/O 密集型任务
CPU 密集型受 GIL 限制明显
纯 Python 计算很难吃满多核心
Python asyncio 协程
通常一个线程里跑一个事件循环
一个事件循环调度很多协程
适合大量 I/O 等待
主要解决并发,不解决多核心并行
Python 多进程
可以吃多核心
隔离性好
但日志、状态、内存、通信更复杂
Go 单进程多 goroutine
goroutine 很轻量
写法接近同步代码
Go runtime 自动调度
可以利用多个 CPU 核心
日志、状态、配置、连接池都好统一
Go 多进程 / 多容器
隔离性更强
可以水平扩展
适合高可用
但日志和状态同步也会变复杂
十二、用 worker 系统举例
假设有一个任务系统:
HTTP API 接收任务
任务进入 Redis Stream
多个 worker 消费任务
任务状态写入 DB
日志记录执行过程
Python 常见写法
如果你想利用多核心,可能会开多个进程:
worker process 1
worker process 2
worker process 3
worker process 4
优点:
隔离性好
一个进程挂了,不一定影响其他进程
可以吃多个 CPU 核心
缺点:
日志不好统一
每个进程有自己的连接池
内存状态不能直接共享
任务状态要依赖外部 Redis/DB
进程间通信麻烦
Go 常见写法
Go 可以一个进程里开多个 goroutine worker:
一个 Go 进程
├── worker goroutine 1
├── worker goroutine 2
├── worker goroutine 3
└── worker goroutine 4
优点:
一个进程内就可以吃多核心
日志统一
状态统一
连接池统一
配置统一
worker 管理简单
缺点:
进程挂了,所有 goroutine 都受影响
一个 panic 没 recover,可能导致整个进程崩溃
单进程内存爆了,全体 worker 都没了
所以 Go 单进程多 goroutine 很爽,但也不是完全没有代价。
十三、Go 单进程 worker 要注意 panic recover
Go 里一个 goroutine 如果 panic 没有 recover,可能导致整个进程退出。
所以 worker 里最好做 per-message recover。
也就是每条任务单独保护:
func (w *Worker) processMessageSafe(ctx context.Context, message *queue.Message) {
defer func() {
if recovered := recover(); recovered != nil {
log.Printf(
"Worker %s panic while processing task %s: %v\n%s",
w.id,
message.TaskID,
recovered,
debug.Stack(),
)
// 把任务标记为 failed
// ack 消息,避免坏任务反复重试卡住队列
}
}()
w.processMessage(ctx, message)
}
这样行为是:
单条任务 panic
-> recover
-> 打日志 + stack trace
-> 标记任务 failed
-> ack 队列消息
-> worker goroutine 继续处理下一条任务
这个模式很重要。
不要只在 Run 顶部 recover。
因为如果 recover 放在 Run 顶部,一次 panic 后,整个 Run 函数会退出,worker 就没了。
更稳的是:
每条消息单独 recover
这样坏任务不会拖死整个 worker,更不会拖死整个进程。
十四、什么时候该用 Python?
不是说 Go 全面替代 Python。
Python 依然非常适合:
AI / ML
数据处理
脚本自动化
快速原型
爬虫
模型推理调用
业务逻辑快速验证
生态依赖 Python 的项目
如果任务主要是:
调用模型
处理数据
接 Hugging Face / PyTorch / numpy / pandas
写脚本
快速验证想法
Python 仍然很香。
而且很多底层库是 C/C++/CUDA 写的,它们可能释放 GIL,或者自己开线程,所以 Python 调这些库时并不一定被 GIL 完全限制。
十五、什么时候该用 Go?
Go 很适合:
后端服务
任务队列 worker
API 网关
高并发网络服务
长连接服务
日志处理服务
部署简单的单二进制服务
需要更好并发模型的系统
尤其是你要写:
一个服务进程
里面多个 worker
统一消费队列
统一日志
统一状态
Docker 部署
只暴露一个端口
Go 的体验会非常舒服。
因为它的模型天然适合:
单进程 + 多 goroutine + 多核心利用
十六、最重要的一张总结表
| 模型 | 是否并发 | 是否多核心并行 | 适合场景 | 主要问题 |
|---|---|---|---|---|
| Python 单进程单线程 | 一般 | 否 | 简单脚本、普通服务 | 性能上限低 |
| Python 多线程 | 是 | CPU 密集型通常不行 | I/O 等待 | 受 GIL 影响 |
| Python asyncio 协程 | 是 | 通常不行 | 高并发 I/O | 阻塞调用会卡事件循环 |
| Python 多进程 | 是 | 是 | CPU 密集型、多 worker | 日志、状态、通信复杂 |
| Go 单进程多 goroutine | 是 | 是 | worker、后端、高并发服务 | 进程级故障影响面大 |
| Go 多容器多副本 | 是 | 是 | 高可用、横向扩展 | 日志采集、状态同步复杂 |
十七、我的当前理解
现在我对这件事的理解是:
Python 的强项不是单进程多线程吃满 CPU。
Python 更常见的路线是:
I/O 用线程或 asyncio,
CPU 和多核心用多进程,
AI/数据处理靠强大的生态和底层 native 库。
而 Go 的路线是:
一个进程里开很多 goroutine。
goroutine 很轻。
runtime 会调度到底层多个线程和多个 CPU 核心。
所以单进程也能做高并发,并且能利用多核心。
所以 Go 在写 worker 服务时非常舒服。
前期可以用:
单 Go 进程
多个 goroutine worker
Redis / DB 保存任务状态
统一 logger
Docker restart 保活
等规模上来,再升级成:
多个 Go 容器副本
每个副本内部多个 goroutine worker
日志写 stdout
Docker / Loki / ELK / journald 统一收集
不要一开始就过度架构。
单机单服务阶段,Go 单进程多 goroutine 已经很强。
总结
最后用几句话收住。
第一:
并发不是并行。
并发是多个任务都在推进。
并行是多个任务真的同时在多个 CPU 核心上执行。
第二:
Python asyncio 协程通常是在一个线程里的事件循环调度很多协程。
它擅长 I/O 并发,不擅长 CPU 多核心并行。
第三:
CPython 的 GIL 限制的是:
同一个进程里多个线程同时执行 Python 字节码。
所以纯 Python CPU 密集型任务,多线程很难吃满多核心。
第四:
Python 想稳定吃多核心,常见方式是多进程。
但多进程会带来日志、状态、内存、通信复杂度。
第五:
Go 的 goroutine 是轻量并发任务。
一个 Go 进程里可以有很多 goroutine。
Go runtime 可以把它们调度到多个 OS 线程和多个 CPU 核心上。
第六:
Go 单进程多 goroutine 写 worker 很爽:
日志统一,状态统一,配置统一,连接池统一,还能利用多核心。
但也要记住:
Go 单进程的代价是进程级故障影响面更大。
所以 worker 里要做好 recover、任务失败标记、超时控制、状态落库和 Docker restart。
一句话总结:
Python 更像是:多进程解决多核心,协程解决 I/O 并发。
Go 更像是:单进程多 goroutine 同时解决高并发和多核心利用。
这就是我现在觉得 Go 写 worker 服务很爽的根本原因。
陕公网安备61011302002223号