Python 进程、线程、协程到底是什么?一篇讲清楚适用场景和区别

学习 Python 并发时,最容易混在一起的三个概念就是:进程、线程、协程。

很多资料会直接给结论:

  • CPU 密集型用多进程
  • IO 密集型用多线程或协程
  • 大量异步 IO 用协程

这些结论没错,但如果只背结论,面试时很容易被追问卡住。本文用比较直观的方式,把进程、线程、协程的本质、区别、适用场景和 Python 里的典型写法讲清楚。

一、先给结论

类型 谁调度 内存关系 创建和切换成本 适合场景
进程 操作系统 进程间内存隔离 CPU 密集、多核并行
线程 操作系统 同一进程内线程共享内存 阻塞 IO、少量到中等并发
协程 程序事件循环 通常在同一线程内协作执行 大量非阻塞 IO

一句话总结:

进程是资源分配单位,线程是操作系统调度的执行单位,协程是用户态由事件循环调度的轻量任务。

二、什么是进程?

进程是操作系统分配资源的基本单位。

当我们运行一个 Python 脚本:

python sync_baseline.py

操作系统会启动一个 Python 进程。这个进程拥有自己的内存空间、变量、文件句柄和运行环境。

进程之间默认是隔离的。一个进程里的变量,另一个进程不能直接访问。

进程的特点

  • 进程之间内存隔离,稳定性更好。
  • 一个进程崩溃,通常不会直接破坏另一个进程的内存。
  • 进程创建、销毁和切换成本比较高。
  • 多进程可以真正利用多个 CPU 核心。
  • 进程之间通信需要额外机制,例如 Queue、Pipe、Socket、共享内存等。

进程适合什么场景?

进程适合 CPU 密集型任务。

比如:

  • 图片处理
  • 视频转码
  • 大批量数据计算
  • 压缩和解压
  • 复杂文本解析
  • 机器学习中的特征处理

这些任务的特点是:CPU 一直在忙着算。

Python 中由于 GIL 的存在,纯 Python 多线程很难让多个 CPU 核心同时执行 Python 字节码。而多进程是多个独立的 Python 解释器进程,可以绕开 GIL 的限制。

多进程示例

from concurrent.futures import ProcessPoolExecutor


def cpu_task(n: int) -> int:
    return sum(i * i for i in range(n))


def main() -> None:
    numbers = [1_000_000, 1_000_000, 1_000_000, 1_000_000]

    with ProcessPoolExecutor() as pool:
        results = list(pool.map(cpu_task, numbers))

    print(results)


if __name__ == "__main__":
    main()

这个例子中,每个 CPU 计算任务可以分配给不同进程执行,从而利用多核。

三、什么是线程?

线程是进程里的执行单位。

一个进程里可以有多个线程。它们共享同一个进程的内存空间。

可以这样理解:

  • 进程像一个公司。
  • 线程像公司里的员工。
  • 公司有自己的办公室、电脑、资料柜。
  • 员工共享这些资源,但每个人可以处理不同任务。

线程的特点

  • 多个线程共享同一个进程的内存。
  • 创建和切换成本低于进程。
  • 线程由操作系统调度。
  • 线程之间共享变量方便,但也容易出现线程安全问题。
  • Python 纯 CPU 密集型多线程会受到 GIL 限制。
  • 对阻塞 IO 任务,多线程仍然有明显价值。

线程适合什么场景?

线程适合阻塞 IO。

比如:

  • 使用 requests 请求多个 URL
  • 读写多个文件
  • 等待数据库查询返回
  • 调用第三方接口
  • 少量到中等数量的并发任务

这些任务的特点是:程序大部分时间不是在计算,而是在等待外部系统返回。

多线程示例

from concurrent.futures import ThreadPoolExecutor
import time


def fetch(index: int) -> str:
    time.sleep(0.2)
    return f"ok-{index}"


def main() -> None:
    with ThreadPoolExecutor(max_workers=5) as pool:
        results = list(pool.map(fetch, range(10)))

    print(results)


if __name__ == "__main__":
    main()

这里的 time.sleep(0.2) 模拟一次阻塞 IO。

如果同步执行 10 次,大约需要 2 秒。如果开 5 个线程,多个任务可以同时等待,总耗时会明显降低。

为什么说线程适合 IO?

因为 IO 等待期间,线程并没有一直占用 CPU。

比如线程 A 正在等待网络返回,这时操作系统可以切换到线程 B 执行。这样多个任务的等待时间可以重叠。

所以,线程不是没用。GIL 主要限制的是 CPU 密集型 Python 字节码并行,不代表线程不能提升 IO 密集任务的吞吐。

四、什么是协程?

协程是用户态的轻量级任务,常见于 Python 的 asyncio

线程由操作系统调度,协程由程序里的事件循环调度。

协程不会自动并行。它是在遇到 await 时主动让出控制权,让事件循环去执行其他已经就绪的协程。

协程的特点

  • 非常轻量,可以创建大量协程。
  • 切换成本低,因为不需要操作系统线程切换。
  • 适合大量非阻塞 IO。
  • 需要 async / await 调用链支持。
  • 如果在协程里直接调用阻塞函数,会卡住整个事件循环。

协程适合什么场景?

协程适合大量非阻塞 IO。

比如:

  • 异步 HTTP 请求
  • WebSocket 长连接
  • 异步数据库访问
  • 异步爬虫
  • FastAPI 异步接口中的外部 IO

协程的前提是:你使用的库本身要支持异步。

例如:

  • httpx.AsyncClient
  • aiohttp
  • asyncpg
  • aiomysql

如果你在协程里直接调用 requests.get() 这种阻塞函数,事件循环仍然会被卡住。

协程示例

import asyncio


async def fetch(index: int) -> str:
    await asyncio.sleep(0.2)
    return f"ok-{index}"


async def main() -> None:
    tasks = [fetch(i) for i in range(10)]
    results = await asyncio.gather(*tasks)
    print(results)


if __name__ == "__main__":
    asyncio.run(main())

这里的 await asyncio.sleep(0.2) 不是阻塞当前线程,而是告诉事件循环:

我现在要等 0.2 秒,你先去执行别的协程。

所以 10 个协程可以几乎同时开始等待,总耗时接近 0.2 秒,而不是 2 秒。

五、用同步 IO baseline 理解三者差异

假设有 10 个任务,每个任务都要等待 0.2 秒。

同步代码大概是这样:

import time


def fetch_sync(index: int) -> str:
    time.sleep(0.2)
    return f"ok-{index}"


def main() -> None:
    results = [fetch_sync(i) for i in range(10)]
    print(results)


if __name__ == "__main__":
    main()

执行顺序是:

任务 1 等 0.2 秒
任务 2 等 0.2 秒
任务 3 等 0.2 秒
...
任务 10 等 0.2 秒

所以总耗时接近:

10 * 0.2 = 2 秒

如果换成线程:

线程 A 在等 IO
线程 B 也在等 IO
线程 C 也在等 IO

多个等待可以重叠,总耗时会下降。

如果换成协程:

协程 A await IO
协程 B await IO
协程 C await IO

协程在 await 时把控制权交回事件循环,事件循环继续推进其他协程,总耗时也会下降。

六、GIL 对线程有什么影响?

GIL 是 Global Interpreter Lock,全局解释器锁。

在 CPython 中,同一时刻通常只有一个线程能执行 Python 字节码。

这意味着:

  • 多线程不适合纯 Python CPU 密集计算。
  • 多线程仍然适合 IO 密集任务。

为什么?

因为 IO 等待时,线程会释放 CPU,操作系统可以调度其他线程。对于网络请求、数据库等待、文件读写等场景,线程依然能提高吞吐。

所以不能简单地说:

Python 有 GIL,所以线程没用。

更准确的说法是:

GIL 主要限制 CPU 密集型 Python 多线程并行;对于 IO 密集型任务,多线程仍然有实际价值。

七、协程是不是一定比线程快?

不是。

协程快不快,取决于调用链是否真正非阻塞。

如果代码是这样:

async def fetch():
    requests.get("https://example.com")

虽然函数写成了 async def,但里面调用的是阻塞 IO。它会卡住整个事件循环。

正确的异步调用应该使用支持 async 的库,例如:

import httpx


async def fetch():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://example.com")
        return response.text

所以,async def 不等于自动更快。真正关键的是:

  • 是否存在 await
  • await 的对象是否是非阻塞 IO
  • 调用链是否全程支持异步

八、怎么做技术选型?

可以按下面这个顺序判断。

1. 任务是不是 CPU 密集?

如果 CPU 一直在忙着计算,例如图片处理、复杂解析、大批量计算,优先考虑:

多进程
任务队列
分布式任务

2. 任务是不是 IO 密集?

如果任务主要在等待网络、数据库、磁盘或第三方 API,考虑:

线程
协程

3. 依赖库是否支持 async/await?

如果支持,例如 aiohttphttpx.AsyncClientasyncpg,可以优先考虑协程。

如果不支持,例如只能使用 requests 或某些阻塞 SDK,可以考虑线程池。

4. 并发规模有多大?

少量到中等并发,线程池通常更直接。

大量连接、大量等待、大量非阻塞 IO,协程通常更合适。

九、常见误区

误区 1:有 GIL,所以 Python 线程没用

不准确。

GIL 主要影响 CPU 密集任务。IO 密集场景下,多线程仍然能提高吞吐。

误区 2:用了 async def 就一定更快

不准确。

只有调用链里的 IO 操作能 await,协程才有明显收益。

误区 3:进程一定最快

不准确。

进程有创建、序列化、通信成本。大量小任务如果用多进程,可能成本比收益还高。

误区 4:协程能替代所有线程

不准确。

如果依赖库是阻塞的,强行套 async 反而会阻塞事件循环。此时线程池可能更合适。

十、面试怎么回答?

可以这样回答:

进程是操作系统分配资源的基本单位,进程之间内存隔离,适合 CPU 密集型任务,也可以利用多核 CPU。线程是进程内的执行单位,多个线程共享进程内存,适合阻塞 IO,但纯 Python CPU 密集任务会受到 GIL 限制。协程是用户态的轻量级任务,由事件循环调度,适合大量非阻塞 IO,但前提是调用链支持 async/await。实际选型时,我会先判断任务是 CPU 密集还是 IO 密集,再看依赖库是否支持异步,如果是 CPU 密集就考虑多进程,如果是阻塞 IO 可以用线程池,如果是大量非阻塞 IO 则优先用 asyncio、httpx 或 aiohttp。

十一、总结

最后再用一张表收尾:

问题 进程 线程 协程
调度者 操作系统 操作系统 事件循环
是否共享内存 默认不共享 共享进程内存 通常同线程内共享
成本
是否适合 CPU 密集 适合 不适合纯 Python CPU 密集 不适合
是否适合 IO 密集 可以,但成本偏高 适合阻塞 IO 适合非阻塞 IO
典型工具 ProcessPoolExecutor ThreadPoolExecutor asyncio / httpx / aiohttp

最重要的不是记住“哪个更高级”,而是能根据任务瓶颈做判断:

CPU 在忙着算:优先多进程
主要在等外部系统:线程或协程
依赖库支持 async/await:优先协程
依赖库是阻塞调用:线程池更直接
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐