Python 中线程与进程的调度机制(含状态流转与内存分配)

场景:启动一个下载任务(download_file()

创建主线程

分配虚拟内存空间

主线程就绪

CPU 调度器选中

发起系统调用\n(read network socket)

系统中断通知\nI/O 完成

下载完成\n返回结果

title

进程与线程的状态流转及内存分配

进程创建\n(pid=1001)\n堆内存分配

ThreadMain

内存布局占位

就绪队列

运行中\n执行 download_file()

阻塞状态\n等待 I/O 完成

进程退出\n释放所有内存

GIL 存在:同一时间
只有一个线程执行 Python 字节码

内存布局(进程级,所有线程共享):
▪ 代码段 : 只读,存放程序指令
▪ 数据段 : 全局/静态变量
▪ 堆 : 动态分配(如下载缓冲区),多线程共享 → 需锁保护
▪ 栈 : 每个线程私有,保存函数调用上下文
▪ 共享库 : 加载 libc、Python 解释器等

组件 说明
进程创建 每个 Python 脚本运行时生成独立进程,拥有独立的虚拟内存空间
内存布局 包括代码段、数据段、堆(动态分配,如下载缓冲区)、栈(线程私有)
线程(主线程) 默认由进程创建,执行 Python 代码;多线程共享堆内存
GIL(全局解释器锁) 限制同一时刻仅一个线程执行 Python 字节码,影响 CPU 密集型性能
状态流转 就绪 → 运行 → 阻塞 → 就绪 → 运行 → 退出 是典型线程生命周期
阻塞 I/O 如网络读取会导致线程挂起,直到操作系统通知完成

此模型适用于 threading 或同步 requests 下载场景。

线程与事件循环如何配合调度异步任务

场景:提交一个异步下载请求 async_download(url)
结果存储区 操作系统内核 协程 async_download() 任务队列 (Pending Tasks) 事件循环 (Event Loop) 主线程 结果存储区 操作系统内核 协程 async_download() 任务队列 (Pending Tasks) 事件循环 (Event Loop) 主线程 事件循环不阻塞,可处理多个并发请求 所有操作在单线程中完成\n无需锁机制 使用 await 时主动让出控制权 asyncio.run(main()) 启动并注册主任务 提交 async_download("file.txt") 创建协程对象并封装为 Task await aiohttp.get() → 非阻塞 I/O 立即返回 Pending,注册回调 继续处理其他待办任务 I/O 完成(数据到达) 触发回调,恢复协程执行 解析响应 → 保存 result = {"status": 200, ...} 返回数据引用 return result 将最终结果返回给主线程 事件循环如何调度异步任务(模拟提交、执行、返回)
组件 说明
事件循环 (Event Loop) 单线程中的核心调度器,管理异步任务、回调和 I/O 事件
协程 (Coroutine) 使用 async/await 定义的轻量级“伪线程”,可暂停和恢复
任务队列 (Task Queue) 存放待执行或等待唤醒的任务(Tasks),由事件循环轮询
非阻塞 I/O 异步库(如 aiohttp)发起请求后立即返回 Pending,不阻塞线程
回调与恢复 当 I/O 完成时,操作系统通知事件循环,触发协程恢复执行
结果存储区 临时保存异步任务输出,供后续使用或返回给调用者
无锁并发 所有操作在单线程中串行化,避免竞争条件,提升 I/O 并发效率

这是 asyncio + aiohttp 的典型工作模式,适合高并发网络应用。

总结对比

特性 多线程/多进程模型 异步事件循环模型
并发单位 线程 / 进程 协程(轻量级任务)
内存开销 高(每个线程有独立栈) 极低(共享栈上下文)
上下文切换 由操作系统调度,较重 用户态切换,极快
I/O 阻塞 线程会被挂起 协程 await 时让出控制权
并发能力 受限于线程数/GIL 数千级并发连接
典型用途 CPU 密集型 / 同步接口 高并发 I/O(如 Web API)

深入理解:I/O 多路复用 —— select, poll, epoll

场景问题:为什么需要这些机制?

假设你是一个 Web 服务器,要同时处理成千上万个客户端连接:

  • 如果为每个连接创建一个线程?→ 内存开销大、上下文切换成本高。
  • 如果同步读写每个 socket?→ 一旦某个连接无数据,线程就被阻塞,无法服务其他请求。

如何用单线程高效监听多个文件描述符(file descriptors, fd)的 I/O 事件(如“有数据可读”)?

答案就是:I/O 多路复用(I/O Multiplexing)

什么是 I/O 多路复用?

I/O 多路复用 是操作系统提供的一种机制,允许一个进程/线程 同时监视多个文件描述符,并在其中任意一个就绪(如可读、可写)时通知应用程序。

它不是真正的并发,而是 事件驱动 + 非阻塞 I/O 的基础

select:最早的多路复用机制

工作方式
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 传入三组 fd 集合:想监控“可读”、“可写”、“异常”的 fd。
  • 调用后,内核会阻塞直到:
    • 至少一个 fd 就绪
    • 超时
    • 被信号中断
  • 返回后,遍历所有 fd 判断哪个就绪(通过 FD_ISSET()
特点
优点 缺点
跨平台(几乎所有 Unix 系统都支持) 最大监控 fd 数量有限(通常 1024)
实现简单 每次调用都要把整个 fd 集合拷贝到内核
易于理解 返回后需遍历所有 fd 查找就绪者 → O(n) 效率低

不适合高并发场景

poll:对 select 的改进

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

使用数组形式传递 pollfd 结构体,没有 1024 限制。

特点
优点 缺点
无最大 fd 数量限制 仍需每次拷贝全部 fd 到内核
使用结构体更清晰 返回后仍需遍历所有 fd → O(n)
可扩展性更好 性能随连接数增长而下降

select 好,但仍未解决本质性能问题

epoll(Linux 特有):高性能解决方案

💡 epoll = event poll,专为大规模并发设计(如 Nginx、Redis 使用)

三大接口
函数 作用
epoll_create() 创建一个 epoll 实例(返回一个特殊的 fd)
epoll_ctl() 向 epoll 注册/修改/删除 监听的 fd 及其事件(如 EPOLLIN)
epoll_wait() 等待事件发生,只返回就绪的 fd 列表
工作模式
// 1. 创建 epoll 实例
int epfd = epoll_create(1);

// 2. 添加 socket 到监听列表
struct epoll_event ev;
ev.events = EPOLLIN; // 关注可读事件
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

// 3. 循环等待事件
struct epoll_event events[1024];
int n = epoll_wait(epfd, events, 1024, -1); // 阻塞等待

for (int i = 0; i < n; i++) {
    if (events[i].events & EPOLLIN) {
        read(events[i].data.fd, buffer, sizeof(buffer)); // 处理数据
    }
}
特点
优点 说明
无需重复传入所有 fd epoll_ctl() 注册一次即可长期有效
只返回就绪的 fd epoll_wait() 返回的是活跃连接列表 → O(1) 扫描效率
支持边缘触发(ET)和水平触发(LT) ET 更高效,减少重复通知
适用于 C10K/C100K 问题 百万级并发也能轻松应对

epoll 是现代高性能网络服务的核心

事件循环是如何依赖这些机制的?

Python 的 asyncio 事件循环底层正是基于这些系统调用来实现非阻塞 I/O 调度。

示例:asyncio 在 Linux 上的工作流程
import asyncio

async def handle_request(reader, writer):
    data = await reader.read(100)   # ← 这里不会阻塞线程!
    response = "HTTP/1.1 200 OK\r\n\r\nHello"
    writer.write(response.encode())
    await writer.drain()             # ← 等待发送完成
    writer.close()

# 启动服务器
loop = asyncio.get_event_loop()
coro = asyncio.start_server(handle_request, '127.0.0.1', 8080)
server = loop.run_until_complete(coro)
loop.run_forever()
底层发生了什么?
  1. reader.read() → 转换为注册该 socket 的 “可读”事件
  2. 事件循环将这个 fd 添加到 epoll 监控列表中(通过 add_reader(fd, callback)
  3. 当客户端发来数据时,操作系统通知 epoll → 触发事件
  4. epoll_wait() 返回该 fd → 事件循环调用对应的回调函数
  5. 回调恢复协程执行:await reader.read() 完成,继续向下运行

整个过程在单线程中完成,没有线程阻塞!

事件循环工作原理图(Mermaid)

发送缓冲区 协程: handle_client() 事件循环 (Event Loop) epoll 实例 操作系统内核 (TCP/IP 栈) 客户端 发送缓冲区 协程: handle_client() 事件循环 (Event Loop) epoll 实例 操作系统内核 (TCP/IP 栈) 客户端 初始状态:epoll 监听 server_socket 的 EPOLLIN 事件 协程挂起,控制权交还事件循环 如果缓冲区满,await writer.drain() 会等待 EPOLLOUT 事件循环永不阻塞,高效支持数万并发连接 TCP SYN → 建立连接 接受连接,创建 client_socket client_socket 可读事件就绪 epoll_wait() 返回该 socket 启动协程 handle_client(client_socket) await socket.read() → 非阻塞读取 HTTP 请求 继续处理其他任务或调用 epoll_wait() 数据到达,read() 完成 解析请求,准备响应 "Hello, World!" socket.write("HTTP/1.1 200...\r\n\r\nHello") 数据拷贝至内核发送队列 await writer.drain() → 等待可写 缓冲区可写,drain() 完成 关闭连接或保持长连接 协程结束 继续监听新事件 事件循环如何通过 epoll 调度异步协程(完整请求处理流程)

这就是 asyncio 的核心调度逻辑:事件驱动 + 协程挂起/恢复

线程与事件循环的关系

项目 说明
默认情况下,事件循环运行在一个线程中 通常是主线程
一个线程最多有一个活跃的事件循环 asyncio.get_event_loop() 获取当前线程的 loop
你可以创建多个线程,每个线程有自己的事件循环 用于隔离不同任务(如 GUI + 网络)
CPU 密集型任务不应在事件循环线程中直接执行 会阻塞事件循环 → 使用 run_in_executor() 提交到线程池

示例:混合使用线程与事件循环

import asyncio
import time
from concurrent.futures import ThreadPoolExecutor

def blocking_task():
    time.sleep(2)
    return "耗时计算完成"

async def main():
    loop = asyncio.get_event_loop()
    with ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, blocking_task)
        print(result)

asyncio.run(main())

这样就不会阻塞事件循环!

总结对比表

机制 支持平台 最大连接数 时间复杂度 是否被 asyncio 使用
select 所有 Unix ~1024 O(n) 是(跨平台 fallback)
poll 大多数 Unix 无硬限制 O(n) 是(某些系统)
epoll Linux 百万+ O(1) avg ✅ 默认(Linux)
kqueue macOS/BSD 百万+ O(1) avg ✅ 默认(macOS)
IOCP Windows 百万+ O(1) avg ✅ Windows 上等效实现

Python 的 asyncio 会根据操作系统自动选择最优后端(libuv 或内置 selector)。

关键结论

概念 说明
select/poll/epoll 操作系统提供的 I/O 多路复用原语,是异步 I/O 的基石
事件循环 建立在这些系统调用之上,管理协程生命周期与事件回调
协程 用户态轻量级“线程”,通过 await 主动让出控制权
非阻塞 I/O 底层 socket 设为 non-blocking,配合 epoll 实现高效并发
线程 事件循环通常运行在单线程中;可通过线程池处理阻塞操作

线程/进程/协程对比分析

维度 线程(threading 进程(multiprocessing 协程(asyncio
本质 操作系统调度的执行流(轻量级) 独立运行的程序实例(重量级) 用户态协作式任务(极轻量)
所属层级 内核级并发单元 系统级独立运行单元 用户态逻辑单元(由事件循环管理)
是否共享内存 ✅ 是(共享全局变量) ❌ 否(独立地址空间) ✅ 是(单线程内运行)
创建开销 中等(~1MB 栈空间) 高(完整内存镜像复制) 极低(仅函数状态对象)
上下文切换成本 高(涉及内核态切换) 最高(进程间切换昂贵) 极低(纯用户态跳转)
最大并发数 几百 ~ 几千 受 CPU 核心数限制(通常 2–64) 数万 ~ 十万级
调度方式 抢占式(OS 控制) 抢占式(OS 控制) 协作式(程序员控制 await
是否受 GIL 限制 ✅ 是(同一时间仅一个线程执行 Python 代码) ❌ 否(每个进程有独立 GIL) ✅ 不受影响(在单线程中运行,无竞争)
能否实现并行计算 ❌ 不能(GIL 阻止多线程并行) ✅ 能(多进程跨核并行) ❌ 不能(本质是单线程)
I/O 并发能力 ✅ 支持(适合中低并发 I/O) ✅ 支持(但资源浪费大) ✅✅ 强支持(高并发 I/O 最佳选择)
典型用途 中低并发网络请求、GUI 响应、同步库封装 CPU 密集型任务、真正并行处理 高并发 I/O(爬虫、API 网关、WebSocket)

核心差异

1. 并行 vs 并发
模型 是否支持并行? 是否支持并发? 说明
线程 ❌(Python 中因 GIL 无法并行执行 Python 代码) ✅(I/O 时可并发切换) “伪并行”,实为 I/O 并发
进程 ✅(每个进程独立运行,可跨 CPU 核心) ✅(通过多进程实现并发 + 并行) 真正的并行计算解决方案
协程 ❌(运行于单线程) ✅✅(极高效率的 I/O 并发) 仅用于并发,不用于并行

关键认知:

  • 并行 = 同时做多件事(需要多个 CPU 核)
  • 并发 = 快速切换做多件事(可在单核完成)
2. GIL 的真实影响图谱
模型 GIL 影响 解释
线程 ⚠️ 完全受限 多个线程不能同时执行 Python 字节码
进程 ✅ 规避 GIL 每个进程有自己的解释器和 GIL
协程 ✅ 不受影响 所有协程运行在一个线程中,只有一个 GIL,但不会发生争抢

提示:协程之所以“高效”,不是因为它绕过了 GIL,而是因为它根本不需要多个线程来争抢 GIL。

3. 资源占用与扩展性对比
模型 内存占用 扩展性 示例:启动 10,000 个任务
线程 高(约 1MB/线程 → 10GB) 差(系统限制,易崩溃) ❌ 实际不可行
进程 极高(每个进程复制内存) 极差(通常最多几十个) ❌ 完全不可行
协程 极低(~1KB/协程 → ~10MB) 极强(取决于内存和 I/O 能力) ✅ 轻松实现

📊 数据参考:

  • 创建 10,000 个线程:Linux 默认栈大小 8MB → 理论需 80GB 内存(实际会 OOM)
  • 创建 10,000 个协程:总内存增加约 10–50 MB
4. 编程模型与生态兼容性
模型 编程风格 学习曲线 第三方库支持 异常处理难度
线程 同步风格,直观 几乎所有库都可用 中等(需注意锁)
进程 类似线程,但通信复杂 中高 支持,但需序列化数据 中(管道/队列易错)
协程 异步风格(async/await 必须用异步库(如 aiohttp, aioredis 高(异常需 await 才能捕获)

常见陷阱:

  • async 函数中调用 time.sleep()requests.get() → 阻塞整个事件循环
  • 使用同步数据库驱动 → 协程失去并发优势

使用决策树

你的任务是什么?

├── 是 CPU 密集型?(如图像处理、加密、数学运算)
│   └── ✅ 使用 multiprocessing(或 concurrent.futures.ProcessPoolExecutor)

├── 是 I/O 密集型?
│   ├── 并发量 < 100,且依赖同步库(如 requests/pandas)
│   │   └── ✅ 使用 threading(简单可靠)
│   │
│   ├── 并发量 > 1000,且可用异步生态(如 aiohttp/FastAPI)
│   │   └── ✅ 使用 asyncio 协程(高性能)
│   │
│   └── 混合任务(I/O + CPU)
│       └── ✅ 使用 asyncio + run_in_executor(进程池) 
│           (I/O 用协程,CPU 用多进程)

└── 需要长期后台运行 + 多模块解耦?
    └── ✅ 考虑多进程架构(如 Celery worker + Web server 分离)

性能对比示例

import asyncio
import time
import requests
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# 模拟 I/O 任务(真实网络请求)
URL = "https://httpbin.org/delay/1"

# 1. 多线程版本(并发 I/O)
def sync_fetch(url):
    return requests.get(url).status_code

def test_threads(n=100):
    with ThreadPoolExecutor() as ex:
        start = time.time()
        list(ex.map(sync_fetch, [URL] * n))
        print(f"线程版耗时: {time.time() - start:.2f}s")

# 2. 协程版本(高并发 I/O)
import aiohttp

async def async_fetch(session, url):
    async with session.get(url) as resp:
        return resp.status

async def test_async(n=100):
    async with aiohttp.ClientSession() as session:
        tasks = [async_fetch(session, URL) for _ in range(n)]
        start = time.time()
        await asyncio.gather(*tasks)
        print(f"协程版耗时: {time.time() - start:.2f}s")

# 3. 多进程版本(用于 CPU 密集型)
def cpu_task(x):
    return sum(i * i for i in range(x))

def test_processes(n=10):
    with ProcessPoolExecutor() as ex:
        start = time.time()
        list(ex.map(cpu_task, [10_000] * n))
        print(f"多进程版耗时: {time.time() - start:.2f}s")

预期结果:

  • 协程处理 100 个延迟请求:约 1.0–1.5 秒(并发完成)
  • 线程处理相同任务:约 1.2–2.0 秒(略慢于协程)
  • 多进程处理计算任务:显著快于单线程(利用多核)

结论

场景 推荐方案 理由
高并发 I/O(>1k) asyncio + 异步库 资源少、性能高、可扩展性强
中低并发 I/O + 同步库 threading 开发简单,无需改造现有代码
CPU 密集型任务 multiprocessing 唯一能突破 GIL 实现并行的方式
混合任务(I/O + CPU) asyncio + run_in_executor(ProcessPoolExecutor) I/O 用协程,CPU 用进程池,最优组合
超高稳定性服务 多进程架构(如 Gunicorn + Async Workers) 隔离故障,避免单点崩溃

总结

在 Python 中:

  • 协程赢在「并发」(I/O 高效调度),
  • 进程赢在「并行」(CPU 真实多核运算),
  • 线程居中妥协(开发简单但扩展性差);

正确的选择不是“哪个更好”,而是“哪个更适合当前任务”。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐