人人都会AI编程

17.3 网络编程模型:阻塞 IO、非阻塞 IO、IO 多路复用

更新时间:2026-07-12

在网络编程中,一个服务器如何同时处理多个客户端连接,本质上是IO 模型的选择问题。Python 提供了从简单到高效的多种模型,理解它们的差异是写出高性能网络应用的前提。


阻塞 IO(Blocking IO)

最直观、最符合直觉的模型。

  • 工作方式:当程序调用 recv()accept() 时,如果没有数据到达或没有新连接,当前线程会被挂起,CPU 不会分配给这个线程,直到数据就绪才返回继续执行。
  • Python 代码示例(简化)
  import socket
  server = socket.socket()
  server.bind(('0.0.0.0', 8080))
  server.listen(5)
  while True:
      conn, addr = server.accept()   # 阻塞,直到有客户端连接
      data = conn.recv(1024)         # 阻塞,直到收到数据
      conn.send(b'Hello')
      conn.close()
  
  • 问题:上面的例子一次只能处理一个客户端。如果当前连接的客户端迟迟不发送数据,服务端就卡住了,其他客户端无法连进来。
  • 多线程/多进程补丁:常见的弥补方案是为每个连接创建一个线程,让每个线程独立阻塞。这样可以同时处理多个客户端,但线程开销大,C10K 问题(一万并发连接)时线程切换会成为瓶颈。而且 Python 的 GIL 限制了 CPU 密集型多线程的收益,IO 密集型虽然能工作,但资源消耗依然较高。

非阻塞 IO(Non-blocking IO)

让单一线程可以主动询问“你好了没”。

  • 工作方式:把套接字设置为非阻塞模式。此时 recv()accept() 不会等待,立即返回。如果没有数据,就抛出一个异常(如 BlockingIOError)。程序需要在一个循环里不断轮询所有套接字,检查哪个就绪。
  • Python 代码示例(简化)
  import socket
  server = socket.socket()
  server.setblocking(False)        # 设置为非阻塞
  server.bind(('0.0.0.0', 8080))
  server.listen(5)
  connections = []
  while True:
      try:
          conn, addr = server.accept()   # 非阻塞,没连接直接抛异常
          conn.setblocking(False)
          connections.append(conn)
      except BlockingIOError:
          pass
      for conn in connections:
          try:
              data = conn.recv(1024)
              if data:
                  conn.send(b'Hello')
          except BlockingIOError:
              pass
  
  • 优点:单线程就能管理多个连接,没有线程切换开销。
  • 致命缺点忙轮询(busy polling)。即使没有任何数据,循环也在疯狂空转,吃掉 CPU 资源。在真实场景中这绝对不可接受。

IO 多路复用(IO Multiplexing)

让操作系统帮我们监听,哪个准备好了就通知我。

这才是现代高性能网络服务的基石。内核提供机制(如 Linux 的 epoll、macOS 的 kqueue、通用的 select/poll),允许我们把一堆套接字注册给内核,并告诉它“我对读事件感兴趣”。当某个套接字可读时,内核会返回一个就绪列表,我们只处理那些就绪的套接字

  • 核心优势
  • 单线程高效管理海量连接。
  • 没有空转,没有数据时线程会阻塞在复用器调用上,CPU 使用率极低。
  • 事件驱动,只处理活跃连接。
  • Python 实现
  • selectors 模块(标准库)提供高层抽象,自动选择当前平台最优的复用器(EpollSelectorKqueueSelector 等)。
  • 示例结构:
    import selectors
    import socket

    sel = selectors.DefaultSelector()
    server = socket.socket()
    server.bind(('0.0.0.0', 8080))
    server.listen(5)
    server.setblocking(False)
    sel.register(server, selectors.EVENT_READ, data=None)

    while True:
        events = sel.select(timeout=None)   # 阻塞,直到有事件
        for key, mask in events:
            if key.data is None:  # 新连接
                conn, addr = key.fileobj.accept()
                conn.setblocking(False)
                sel.register(conn, selectors.EVENT_READ, data=b'')
            else:  # 已有连接的数据
                conn = key.fileobj
                data = conn.recv(1024)
                if data:
                    conn.send(b'Hello')
                else:
                    sel.unregister(conn)
                    conn.close()
    
  • asyncio 模块也是基于 IO 多路复用(典型用 epoll)实现的,结合 async/await 提供更自然的异步编程体验。

三种模型对比如下

| 模型 | 线程模型 | CPU 消耗 | 并发能力 | Python 典型方案 |
|------|---------|---------|--------|----------------|
| 阻塞 IO | 每连接一线程 | 低(阻塞时不占 CPU) | 受线程数限制 | threading + 阻塞 socket |
| 非阻塞 IO | 单线程忙轮询 | 极高(空转)| 理论无限 | 设置 setblocking(False) + 手动轮询(不推荐) |
| IO 多路复用 | 单线程事件驱动 | 低(只在活动时消耗)| 极高(C10K/C100K) | selectorsasyncio |

实用建议

  • 如果你要写一个真正的网络服务器(哪怕只是几十并发),直接用 asyncio 框架或库,而不是自己从 selectors 开始造轮子。asyncio 已经将 IO 多路复用、事件循环、协程调度封装得非常高效。
  • 阻塞 IO + 线程模型适用于连接数少连接活跃处理逻辑简单的场景(如内部工具、小规模客户端)。
  • IO 多路复用(事件驱动)是 Python 异步生态的根基,理解它有助于你明白 async/await 为什么能支持高并发,以及 fastapiaiohttpwebsockets 等库的性能来源。