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

Mac 上 Skill CLI 无法执行的坑:最后其实一条命令就够了

我在做 Amazon skills 的过程中,逐步把本地 CLI 从 Python 脚本切到 Go 二进制。这样做的好处很明显:用户不用装 Python、不用配依赖,解压 skill 后直接运行。但在 macOS 上,我们反复遇到一个看起来很玄的问题:同一个二进制,在 Linux/Windows 上正常,在 Mac 上就是执行不了。 当时遇到的现象 常见报错大概有几类: * 双击或 agent 调用 CLI 时,系统提示文件来自未知开发者,无法打开。 * 终端里执行时提示 Permission denied。 * 已经 chmod +x 了,仍然被 macOS 拦截。 * Apple

By ladydd

当我把全世界人群的基因 PCA 跑出来后,看见了一个倒 L 型

最近我把之前学的一些分子人类学知识,终于真正落地了。 不是停留在看论文、看别人画图、看别人解释“人群结构”这些概念,而是自己把数据处理完,自己跑 PCA,自己把全世界不同人群放到一张图上。 然后那一刻,我真的被击中了。 图上出现了一个非常漂亮的倒 L 型。 一端是非洲,另一端逐渐拉向东亚,中间有中东、欧洲、南亚、欧亚大陆上的各种过渡人群。它不是那种随机散点图,而是有方向、有骨架、有历史感的结构。 我第一眼看到的时候,脑子里直接冒出一句话: 这不像是一张普通统计图,这像是人类迁徙史在二维空间里留下的影子。 当然,后来我也提醒自己,PCA 不能被过度浪漫化。它不是地图,也不是时间轴,更不是“谁从哪里走到哪里”的直接证据。PCA 本质上是把高维基因差异压缩到几个主成分上,用最大方差方向把样本摊开。它可以帮助我们观察人群结构、相似性、分化和混合,但不能单独承担全部历史解释。PCA 在群体遗传学里常用于观察 population structure

By ladydd

吞吐与延迟:一个厨房比喻讲透性能压测

写于 2026-06-26。背景: MCP 服务 跑在 3 台 ClickHouse(每台 16 核 / 64G,1 分片 3 副本)上。 我们花了一整轮做公网压测,把这套系统的极限、天花板和杠杆全摸清了。这篇把"吞吐 / 延迟 / 排队"这三个最容易混的概念讲透,配我们自己的真实实测数据。 一句话结论 我们这套系统的吞吐天花板 ≈ 76 req/s。 往里塞再多并发(100、200、300、500),每秒"做完"的还是大约 76 个,多出来的全在排队。系统不会崩,只会让每个人等得更久。 天花板能不能抬?

By ladydd

四卡 3090 本地模型部署复盘:Ollama 跑通 35B,以及 GPU0 掉卡问题

这次做的是一轮真实的本地模型部署摸底。 目标不是搭一个临时 Demo,而是把一台四卡 3090 GPU 机器接进自己的日常 AI 使用环境:本机跑 Open WebUI,负责账号、会话和前端配置;GPU 机器只负责模型推理。这样以后换模型、换推理框架、重启服务,都尽量不影响本机的使用入口。 最后结论比较清楚:qwen3.5:35b 的 GGUF Q4_K_M 量化版已经通过 Ollama 跑通,本机 Open WebUI 可以接入,热加载后的聊天速度也能用;但 GPU0 存在明显稳定性问题,重启后能短暂恢复,跑过负载后又会掉到 NVML 异常状态。 状态快照 当前能用的部分: * 本机 Open WebUI 已部署,

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