Python 非阻塞 IO 模型原理与实现解析
本文深入解析 Python 中非阻塞 IO 模型的工作原理,通过 Linux Socket 示例展示如何设置非阻塞模式及处理 BlockingIOError 异常。文章详细阐述了非阻塞 IO 在单线程并发中的应用,指出了 send 阻塞及 CPU 占用过高等缺陷,并引出 select 多路复用作为更优解决方案。
非阻塞 IO 模型概述
非阻塞 IO(non-blocking IO)
Linux 下非阻塞 Socket 处理流程
在 Linux 系统中,可以通过配置 Socket 将其设置为非阻塞模式。当对非阻塞 Socket 执行读取操作时,其处理流程如下:

从流程图可以看出,当用户进程发起 read 调用时,若内核数据尚未就绪,内核不会阻塞用户进程,而是立即返回一个错误码。对用户进程而言,发起 read 后无需等待,即刻获得结果。若结果为错误,用户进程可知数据未准备好,从而可在两次 read 调用间隔内执行其他任务,或再次发起 read。一旦内核数据就绪并再次收到系统调用,内核会将数据拷贝至用户内存(此阶段仍为阻塞操作,涉及本地内存拷贝 copy data),随后返回。
换言之,非阻塞的 recvfrom 系统调用返回后,进程并未被阻塞,内核立即返回。若数据未就绪,则返回错误。进程可在此间隙执行其他操作,随后再次发起 recvfrom 调用。此过程通常称为轮询:循环检查内核数据,直至数据就绪,再拷贝至进程进行处理。需注意,数据拷贝阶段进程仍处于阻塞状态。因此,在非阻塞 IO 中,用户进程需主动、频繁地询问内核数据是否就绪。
Python 非阻塞 IO 代码实现
非阻塞 IO 代码示例
- 将 Socket 接口设置为非阻塞 IO 模式
- 默认值为 True,表示阻塞模式
- server.setblocking(False)
- 需处理由此引发的异常
BlockingIOError: [WinError 10035] 无法立即完成一个非阻止性套接字操作。
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8000))
server.listen(5)
# 设置socket接口为 非阻塞IO接口
# 默认是True 为阻塞
server.setblocking(False)
print("starting...")
while True:
try:
conn,addr = server.accept()
print(addr)
except BlockingIOError:
print("干其他的工作")
server.close()
执行结果如上图所示,程序持续返回错误消息。
starting...
干其他的工作
干其他的工作
干其他的工作
干其他的工作
服务端可与多个客户端建立连接,实现持续接收新连接的能力。
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8000))
server.listen(5)
# 设置socket接口为 非阻塞IO接口
# 默认是True 为阻塞
server.setblocking(False)
r_list = []
print("starting...")
while True:
try:
conn,addr = server.accept()
r_list.append(conn)
print(r_list)
except BlockingIOError:
pass
server.close()
启动三个客户端与服务端建立连接。

r_list 列表用于存储所有已建立的连接对象。
若有新连接到来,则建立连接;若无连接,则抛出异常。
通过实现 IO 非阻塞,达成单线程下的并发多连接处理。
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8000))
server.listen(5)
# 设置socket接口为 非阻塞IO接口
# 默认是True 为阻塞
server.setblocking(False)
r_list = []
print("starting...")
while True:
try:
conn,addr = server.accept()
r_list.append(conn)
print(r_list)
except BlockingIOError:
# 定义删除连接列表
del_rlist = []
for conn in r_list:
try:
data = conn.recv(1024)
# 收空数据时候
if not data:
del_rlist.append(conn)
continue
conn.send(data.upper())
# 没有连接,抛出异常,就结束这次循环,继续
except BlockingIOError:
continue
# 套接字出现异常,客户端单方面连接断开
except Exception:
conn.close()
del_rlist.append(conn)
break
# 结束上面循环之后,循环del_list 连接元素 删除连接
for conn in del_rlist:
del_rlist.remove(conn)
server.close()
非阻塞 IO 的缺陷与局限性
潜在 BUG:send 操作同样是阻塞式 IO 接口
当发送数据量较大时,send 操作也会发生阻塞。
send 操作是将应用程序数据发送至操作系统缓冲区,而操作系统缓冲区空间有限。当缓冲区满时,后续数据需等待缓冲区释放空间后方可发送,从而导致阻塞。
修改后的服务端代码可自主检测 IO 状态,遇到 IO 阻塞时切换至线程其他任务执行,从而实现单线程并发处理。
from socket import *
server = socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8000))
server.listen(5)
# 设置socket接口为 非阻塞IO接口
# 默认是True 为阻塞
server.setblocking(False)
r_list = []
w_list = []
print("starting...")
while True:
try:
conn,addr = server.accept()
r_list.append(conn)
print(r_list)
except BlockingIOError:
# 收消息
# 定义删除连接列表
del_rlist = []
for conn in r_list:
try:
data = conn.recv(1024)
# 收空数据时候
if not data:
del_rlist.append(conn)
continue
'''加入元祖 元祖有两个元素
1.存放套接字连接
2.准备要发送的的数据
'''
w_list.append((conn, data.upper()))
# 没有连接,抛出异常,就结束这次循环,继续
except BlockingIOError:
continue
# 套接字出现异常,客户端单方面连接断开
except Exception:
conn.close()
del_rlist.append(conn)
break
# 发消息
# 用于 发成功数据后,删除套接字连接的列表
del_wlist = []
for item in w_list:
try:
conn = item[0]
data = item[1]
conn.send(data)
# 发成功后,从列表删除连接
del_wlist.append(item)
# send 有可能出现异常 没发完情况
except BlockingIOError:
pass
# 结束上面循环之后,循环del_wlist 连接元素 删除连接
for item in del_wlist:
del_wlist.remove(item)
# 结束上面循环之后,循环del_rlist 连接元素 删除连接
for conn in del_rlist:
del_rlist.remove(conn)
server.close()
这便是非阻塞 IO 的工作机制。
然而,非阻塞 IO 模型通常不被推荐作为生产环境的首选方案。不可否认其优点:在等待任务完成期间,可执行其他任务(包括提交其他任务,即允许“后台”多个任务看似“同时”执行)。
在执行其他任务期间,若有新连接到来,服务端可能无法及时响应并建立连接,从而导致响应延迟问题。
此外,该模型存在显著缺点:
1. 循环调用 recv() 会大幅提高 CPU 占用率。这也是代码中保留 time.sleep(2) 的原因,否则在低配置主机上极易导致系统卡顿。
2. 任务完成的响应延迟增加。由于仅在固定时间间隔内轮询一次 read 操作,而任务可能在两次轮询间的任意时刻完成,这会导致整体数据吞吐量降低。
3. 死循环 While True 会导致 CPU 资源的无效消耗和占用。
替代方案:select 多路复用
此外,在此方案中 recv() 主要起到检测“操作是否完成”的作用。实际上,操作系统提供了更高效的检测接口,例如 select() 多路复用模式,可一次性检测多个连接是否活跃。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













