并发编程基础:进程、线程与 GIL 的那些事

各位同学,咱们今天聊聊并发编程的根基。说实话,我见过太多人一上来就撸线程池、搞异步,结果线上出了诡异问题,查半天才发现是基础概念没吃透。这一章,咱们就把进程、线程、GIL 和同步异步模型彻底掰扯清楚。

进程与线程:到底差在哪?

先问个问题:你打开一个浏览器,又打开一个 IDE,这俩是啥关系?进程。那浏览器里开 10 个标签页呢?早期是 10 个进程,后来 Chrome 优化了,但本质上每个标签页还是独立进程。而一个进程里同时干好几件事——比如 IDE 一边打字、一边语法高亮、一边后台编译——这些就是线程。

我习惯用一个比喻:进程是工厂车间,线程是车间里的工人。车间有自己的场地、设备和原材料(内存、文件句柄),工人共享车间的资源,但每个工人有自己的工具(栈、寄存器)。

对比维度 进程 线程
资源开销 大(独立地址空间、PCB) 小(共享进程资源)
创建速度 慢(fork/copy 开销) 快(轻量级)
通信方式 IPC(管道、消息队列、共享内存) 直接读写共享变量
上下文切换 慢(切换页表、刷新 TLB) 快(仅切换寄存器、栈)
隔离性 强(一个进程崩了不影响其他) 弱(一个线程崩了可能拖垮整个进程)
适用场景 CPU 密集型、需要强隔离 I/O 密集型、需要共享数据

我在项目中遇到过一个问题:一个 Python 写的爬虫服务,用多线程抓取上千个 URL,结果 CPU 占用率一直上不去,吞吐量还不如单线程。当时排查了半天,最后发现是 GIL 锁 在作祟。

GIL 锁:Python 并发的心头痛

GIL,全称 Global Interpreter Lock,全局解释器锁。说白了,CPython 解释器里有一把大锁,同一时刻只允许一个线程执行 Python 字节码。你开 8 个线程,它们并不能同时在 8 个核上跑 Python 代码,而是轮流抢这把锁。

核心结论:Python 多线程在 CPU 密集型任务上是「假并发」,本质是串行。但在 I/O 密集型任务上,因为线程在等待 I/O 时会释放 GIL,所以多线程仍然有效。

为什么会这样?CPython 的内存管理不是线程安全的。如果不加 GIL,多个线程同时操作引用计数,内存分分钟崩给你看。这是历史包袱,也是设计取舍。

我曾经在一个数据处理服务里,用 8 个线程做 CPU 密集的数值计算,结果 8 个线程跑出来的时间比单线程还慢——因为线程切换和锁竞争带来了额外开销。后来换成多进程(multiprocessing 模块),每个进程有自己的 GIL,8 个核全跑满,速度直接翻了 6 倍多。

避坑指南:我曾经天真地以为 Python 多线程能线性提升 CPU 密集型任务的性能,结果被 GIL 狠狠教育了一顿。记住:CPU 密集型用多进程,I/O 密集型用多线程。如果你非要在 Python 里做并行计算,考虑用 concurrent.futures.ProcessPoolExecutor 或者直接上 C 扩展(如 NumPy)。

来看一段代码,直观感受 GIL 的影响:

import threading
import time

def cpu_bound(n):
    """CPU 密集型:计算斐波那契数列"""
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

def io_bound(seconds):
    """I/O 密集型:模拟网络请求"""
    time.sleep(seconds)
    return seconds

# 测试 CPU 密集型
start = time.time()
threads = [threading.Thread(target=cpu_bound, args=(500000,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f"CPU 密集型 4 线程耗时: {time.time() - start:.2f}s")

# 测试 I/O 密集型
start = time.time()
threads = [threading.Thread(target=io_bound, args=(2,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f"I/O 密集型 4 线程耗时: {time.time() - start:.2f}s")

你猜结果如何?CPU 密集型 4 线程可能比单线程还慢,而 I/O 密集型 4 线程耗时接近 2 秒(单线程需要 8 秒)。这就是 GIL 的「偏心」——它不耽误你等 I/O,但绝不让你并行算数。

同步与异步模型:两种思维模式

聊完进程线程,咱们再来看一个更抽象的概念:同步 vs 异步。这俩词在很多语境下含义不同,咱们从「调用模型」的角度来理解。

同步模型:你发起一个任务,必须等它做完才能干下一件事。就像你去餐厅点餐,站在柜台前等厨师做好,端走,再点下一道。

异步模型:你发起一个任务,不用等它完成,直接去干别的事。等任务完成了,通过回调、事件或 Future 来通知你。就像你点完餐拿个号,回座位刷手机,叫到号再去取餐。

你想想看,在 I/O 密集型的服务里,同步模型有多浪费?线程发一个网络请求,然后傻等几百毫秒,这期间啥也干不了。异步模型就不一样了,发完请求立刻去处理其他请求,等数据回来了再接着处理。

我习惯用一张图来展示这两种模型的区别:

同步模型 vs 异步模型 同步模型 线程 1:发送请求 → 等待响应 线程 1:阻塞等待(啥也不干) 线程 1:收到响应,继续处理 异步模型 线程 1:发送请求(不等待) 线程 1:处理其他请求(不阻塞) 事件通知:收到响应,回调处理 核心差异 同步:线程在等待 I/O 时被阻塞,浪费 CPU 时间片 异步:线程在等待 I/O 时去干别的事,提高资源利用率

在 Python 里,异步模型主要通过 asyncio 实现。它用事件循环(Event Loop)来调度协程(Coroutine),一个线程就能处理成千上万个并发连接。你想想看,如果每个连接开一个线程,10000 个连接就得 10000 个线程,光线程栈内存就吃掉几个 GB。而异步模型,一个线程 + 一个事件循环,轻松搞定。

我的建议:对于高并发 I/O 服务(如 Web 服务器、API 网关、消息推送),优先考虑异步模型。Python 的 asyncio + aiohttp 或者 FastAPI(基于 Starlette)都是成熟方案。但要注意,异步代码里不能有阻塞调用(比如 time.sleep()、同步数据库查询),否则会阻塞整个事件循环,性能还不如同步模型。

最后,咱们用一张表总结一下三种并发模型的选择:

场景 推荐模型 Python 实现 注意事项
CPU 密集型 多进程 multiprocessing、concurrent.futures.ProcessPoolExecutor 进程间通信开销大,注意数据序列化
I/O 密集型(高并发) 异步 I/O asyncio、aiohttp、FastAPI 避免阻塞调用,注意回调地狱
I/O 密集型(中等并发) 多线程 threading、concurrent.futures.ThreadPoolExecutor GIL 限制,适合 I/O 等待多的场景
混合型 多进程 + 异步 每个进程内跑事件循环 架构复杂,需要权衡