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 单进程多 goroutineworker、后端、高并发服务进程级故障影响面大
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 服务很爽的根本原因。

Read more

传统 SaaS 转向 AI 时代,我目前的一点理解:先把数据能力变成 Agent 可调用的基础设施

最近我一直在思考一个问题:传统 SaaS 到底应该怎么转向 AI? 一开始很容易想到的方向是:给原来的系统加一个 AI 助手。 比如在页面右下角放一个聊天框,让用户可以问数据、生成报告、总结内容、解释指标。这个当然有价值,但我现在越来越觉得,这只是比较表层的一种转型。 真正的变化,可能不是“在 SaaS 里面加 AI”,而是 SaaS 本身的能力形态发生变化。 过去的 SaaS,核心是给人使用。 人登录系统,看页面、点按钮、筛选数据、导出报表、判断问题,然后再去做决策。数据库是给 Web 页面供数的,后端 API 是给前端页面服务的,整个产品的中心是“人如何操作软件”。 但 AI 时代,尤其是 Agent 逐渐发展之后,

By ladydd

对 Python 应用场景的一次重新思考:FastAPI、协程、线程、数据库与任务系统边界

最近在重新设计一个任务系统时,我顺便把自己对 Python,尤其是 CPython 应用场景的理解重新梳理了一遍。 这次讨论的背景是一个典型的异步任务服务: 上游提交任务 API 立即返回 task_id 后台 worker 慢慢执行 用户通过 task_id 查询任务状态 任务主要是 LLM 调用、图片下载、外部 HTTP 请求这类 I/O 型工作。 一开始关注的是队列、Redis、PostgreSQL、worker 并发控制这些问题。但聊到后面,其实更核心的问题变成了: Python 到底应该放在什么位置? 哪些并发适合 Python? 哪些并发不要硬塞给 Python? FastAPI、协程、线程、数据库之间应该怎么分工? 这篇文章就是这次思考的整理。 一、我不想抛弃 Python,

By ladydd

Python 进程和 Go 进程的区别:为什么 Go 单进程多 worker 用起来更爽?

最近我在做 worker 任务系统的时候,突然意识到一个很关键的问题: 以前写 Python,多 worker 的时候经常要小心日志串、文件切割乱、时间不好管理。 但是换成 Go 以后,一个进程里开多个 goroutine worker,反而可以比较自然地写到同一个日志文件里。 一开始我以为这是“Python 和 Go 写日志能力不一样”,后来想明白了,核心不是日志本身,而是: Python 常见 worker 模型:多进程 Go 常见 worker 模型:单进程 + 多 goroutine 这背后其实是两个语言在并发模型上的巨大差异。 一、进程、线程、goroutine 先分清楚 先把几个概念捋一下。 进程:操作系统分配资源的单位 线程:CPU 调度执行的基本单位

By ladydd

从 Redis + Channel 到 Redis Stream:一次 Go 任务队列方案的重新理解

在讨论 PostgreSQL queue 方案之后,我又回头看了一下之前自己设想过的 Go + Redis 任务架构。 最早我脑子里的方案大概是: POST 创建任务 Redis 存状态 Go goroutine 直接处理 进程内 channel 控制并发 这个方案很直观,也很符合 Go 的写法。 用户请求进来后,API 生成一个 task_id,把任务状态写进 Redis,然后把任务塞进 Go 的 channel,由后台 goroutine 消费执行。并发控制就靠一个固定大小的 channel 或 worker pool,比如最多 20 个 goroutine 同时执行任务。 这个设计在单进程里确实很舒服。 但当我开始思考多进程、多容器、

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号