最近研究了一下python 协程(Coroutine)相关的东东,为了搞清本质,又总结了fd、select、poll、epoll这些linux中比较底层的技术,在这里通过一个开快递驿站通俗的例子把这些知识点串联到一起跟大家一起分享!

想象你在城市里开了一个快递驿站,每天要处理成千上万的包裹收发。
你的目标是:用最少的人力(资源),最快地服务最多的客户(连接)。

文件描述符(fd) = 客户的快递柜编号

  • 每个客户来寄/取快递,你给他分配一个唯一的柜子号,比如 101、205、309……
  • 这个柜子号就是 文件描述符(fd)。
  • 它代表一个“正在处理中的 I/O 任务”:
    • 网络连接 → 一个 socket → 一个 fd
    • 打开的文件 → 一个文件句柄 → 一个 fd
    • 管道、设备等 → 都对应一个 fd

fd 就是操作系统给每个“打开的 I/O 资源”发的身份证号。

 问题来了:怎么知道哪个柜子“有事”?

客户寄了包裹,但快递还没到(比如你发 HTTP 请求,对方还没回)。
你不能一直盯着每个柜子看(太浪费人力),也不能让客户干等(体验差)。

于是你需要一个高效的监控机制。

方案一:select —— “老班长点名法” (过时)

  • 你拿着一张全名单(所有柜子号),挨个问:“101 到了吗?205 到了吗?309 到了吗?……”
  • 即使只有 1 个包裹到了,你也得问完全部 10,000 个。
  • 而且名单最多只能写 1024 人(硬限制)。

缺点:效率低、规模小、重复劳动。

方案二:poll —— “升级版点名”(稍好但本质一样)

  • 名单不再限 1024 人,可以用大本子记。
  • 但还是得挨个问一遍,CPU 累死。

仍是 O(n) 轮询,不解决根本问题。

方案三:epoll —— “智能通知系统” (Linux 高性能核心)

你做了三件事:

第一步:建一个“智能登记台”(epoll_create
  • 创建一个中央控制系统(返回一个特殊的 fd,叫 epfd)。
第二步:客户登记需求(epoll_ctl
  • 客户说:“我等 101 号柜的包裹,到了通知我。”
  • 你把 101 号 + 事件类型(比如‘可读’= 包裹到了) 存进系统。
  • 内核用红黑树高效管理这些登记(插入/查找快)。
第三步:坐等通知(epoll_wait
  • 你去喝茶,系统自动监控。
  • 一旦任意柜子有包裹到达,内核立刻把“就绪的柜子号”放进一个就绪队列。
  • 你只处理这个队列里的柜子,其他时间完全空闲!

优势:

  • 不用轮询,只处理有事的连接
  • 支持几十万并发
  • CPU 几乎不空转

这就是 Nginx、Redis、Kafka 等高性能服务在 Linux 上跑得飞快的秘密!

 Python中asyncio 就是“智能调度员”

一个经典的例子

import asyncio
import time

# 定义异步函数
async def task(name, delay):
    print(f"任务 {name} 开始执行,等待 {delay} 秒")
    # 异步休眠:不阻塞其他任务
    await asyncio.sleep(delay)
    print(f"任务 {name} 执行完成")

# 主入口函数
async def main():
    start_time = time.time()

    # 并发运行多个异步任务
    task1 = asyncio.create_task(task("A", 2))
    task2 = asyncio.create_task(task("B", 3))
    task3 = asyncio.create_task(task("C", 1))

    # 等待所有任务结束
    await task1
    await task2
    await task3

    end_time = time.time()
    print(f"总耗时:{end_time - start_time:.2f} 秒")

# 启动异步程序
if __name__ == "__main__":
    asyncio.run(main())

核心知识点

  1. async def:声明这是异步函数,不能直接调用阻塞执行
  2. await:暂停当前协程,让出 CPU,等待 IO 操作完成
  3. asyncio.create_task():创建协程任务,实现并发
  4. asyncio.run():Python3.7+ 标准启动异步入口

跟前面提到的快递驿站进行比较

  • async def task() 就像一个“客户服务流程”。
  •  await  asyncio.sleep(delay),相当于说:“我等 101 号柜的包裹,先挂起。”
  • 事件循环(event loop)就是那个使用 epoll 的调度员:
    • 把 socket fd 注册到 epoll
    • 调用 epoll_wait() 等待
    • 包裹一到,立刻唤醒对应的协程继续执行

整个过程:单线程 + epoll 监控 + 协程切换 = 高效并发

其他平台类似epoll的东东叫什么

操作系统 “智能通知系统”叫什么? 特点
Linux epoll 高效、主流
macOS / BSD kqueue 和 epoll 类似,同样高效
Windows IOCP(I/O Completion Ports) 更底层的真异步,性能同样顶级

Python 3.8+ 开始,asyncio 在 Windows 上默认使用 ProactorEventLoop,它基于 IOCP 实现。

1# 在 Windows 上(Python 3.8+)
2import asyncio
3print(asyncio.get_event_loop()) 
4# 输出:<ProactorEventLoop running=False>

结论

  • 旧版 Python(<3.8):Windows 默认用 select,确实性能差(最大 509 个 socket)。
  • 新版 Python(≥3.8):Windows 默认用 IOCP,性能与 Linux 的 epoll 相当!

性能对比(现代环境)

场景 Linux (epoll) Windows (IOCP)
10,000 并发 HTTP 请求 极快 极快
内存占用
最大连接数 数十万 数十万
实际吞吐量 几乎相同 几乎相同

实测表明:用 aiohttp 或 httpx 写的异步爬虫,在 Windows 10/11 + Python 3.10+ 上性能完全可用。

协程 vs 线程

对比项 线程(Thread) 协程(Coroutine)
谁调度? 操作系统(强制切换) 你自己(await 主动让出)
切换成本 高(保存寄存器、栈、内核态) 极低(只保存 Python 函数状态)
能开多少? 几百个就吃不消 轻松 10w+
适合干啥? CPU 计算(但受 GIL 限制) 网络、文件等 I/O 等待

协程不是“更快的线程”,而是“更适合 I/O 的协作单元”。

 总结

操作系统用 fd 标识每个 I/O 资源 →

 epoll(或 kqueue/IOCP)高效监控哪些 fd 就绪 →

Python 的 asyncio 事件循环利用这一机制,在单线程中通过协程实现高并发 I/O 处理。

可以这样记住:

  • fd → 快递柜编号(资源 ID)
  • epoll → 智能通知系统(只告诉你“有事的柜子”)
  • 事件循环 → 使用 epoll 的调度员
  • 协程 → 可暂停的服务流程(await = “我等通知,先干别的”)
  • asyncio → Python 封装好的全套解决方案(跨平台、开箱即用)

现在,当你再听到“epoll 是 Linux 高性能网络的基石”时,脑海里就可以浮现出那个高效运转的智能快递驿站了!

更多推荐