IO模型详解:从阻塞到事件驱动
事件驱动模型概述
协程在遇到IO操作时会进行切换,但何时切换回来以及如何判断IO操作完成呢?
许多开发者会考虑使用"线程池"或"连接池"来优化系统性能。线程池通过维护适量线程来减少创建销毁开销,连接池则重用已有连接来降低资源消耗。
这些技术虽能缓解IO密集场景的压力,但在面对大规模并发请求时仍有局限。协程结合非阻塞IO是更好的解决方案。
传统编程模式
开始--->代码块A--->代码块B--->代码块C--->代码块D--->......--->结束
传统编程遵循线性执行流程,控制流由程序逻辑和输入数据决定。
事件驱动编程模式
开始--->初始化--->等待
事件驱动程序启动后进入等待状态,直到被外部事件触发执行相应处理逻辑。
事件驱动实现机制
鼠标点击事件示例
<!DOCTYPE html>
<html>
<head>
<title>事件示例</title>
</head>
<body>
<p onclick="handleClick()">点击测试</p>
<script>
function handleClick() {
alert('事件触发!')
}
</script>
</body>
</html>
轮询方式的缺陷
- CPU资源浪费严重
- 阻塞状态下无法处理其他设备
- 响应时间随设备数量增加而延长
事件驱动优势
- 事件队列存储待处理事件
- 事件触发时向队列添加记录
- 循环从队列取出事件并调用对应处理函数
- 每个事件绑定独立处理函数指针
IO多路复用基础
核心概念
- 用户空间与内核空间: 32位系统中4GB虚拟地址空间,高1GB为内核空间,低3GB为用户空间
- 进程切换: 内核挂起运行进程并恢复其他进程的机制
- 进程阻塞: 进程主动进入等待状态,不占用CPU资源
- 文件描述符: 内核维护的文件引用索引
- 缓存IO: 数据先拷贝到内核缓冲区再传输到用户空间
五种IO模型对比
阻塞IO (Blocking IO)
默认socket操作模式,两个阶段都阻塞:
- 等待数据准备就绪
- 将数据从内核拷贝到用户空间
非阻塞IO (Non-blocking IO)
设置socket为非阻塞模式:
import socket
server = socket.socket()
server.setblocking(False) # 设置非阻塞
IO多路复用 (IO Multiplexing)
使用select/epoll同时监控多个连接:
import select
readable, writable, exceptional = select.select(inputs, outputs, inputs)
信号驱动IO (Signal Driven IO)
通过信号通知IO事件完成。
异步IO (Asynchronous IO)
内核完成所有操作后通知应用程序。
Select/Poll/Epoll详解
Select机制
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
缺点:
- 文件描述符数量限制(默认1024)
- 每次调用需拷贝fd集合到内核
- 线性遍历所有fd开销大
Poll机制
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
改进:
- 无文件描述符数量限制
- 统一数据结构管理事件
Epoll机制
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
优势:
- 支持大量文件描述符
- 事件驱动通知机制
- 内存映射减少拷贝开销
Python实践示例
Select服务器实现
import socket
import select
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('localhost', 8080))
server.listen(5)
server.setblocking(False)
inputs = [server]
outputs = []
while inputs:
readable, writable, exceptional = select.select(inputs, outputs, inputs, 1)
for sock in readable:
if sock is server:
conn, addr = sock.accept()
conn.setblocking(False)
inputs.append(conn)
else:
data = sock.recv(1024)
if data:
sock.send(data.upper())
else:
inputs.remove(sock)
sock.close()
Epoll服务器实现
import socket
import select
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('localhost', 8080))
server.listen(5)
server.setblocking(False)
epoll = select.epoll()
epoll.register(server.fileno(), select.EPOLLIN)
connections = {}
requests = {}
while True:
events = epoll.poll(1)
for fileno, event in events:
if fileno == server.fileno():
conn, addr = server.accept()
conn.setblocking(False)
epoll.register(conn.fileno(), select.EPOLLIN)
connections[conn.fileno()] = conn
elif event & select.EPOLLIN:
data = connections[fileno].recv(1024)
if data:
epoll.modify(fileno, select.EPOLLOUT)
requests[fileno] = data
else:
epoll.unregister(fileno)
connections[fileno].close()
del connections[fileno]
elif event & select.EPOLLOUT:
connections[fileno].send(requests[fileno])
epoll.modify(fileno, select.EPOLLIN)
del requests[fileno]
Selectors高级封装
import selectors
import socket
selector = selectors.DefaultSelector()
def accept_connection(server_socket, mask):
conn, addr = server_socket.accept()
conn.setblocking(False)
selector.register(conn, selectors.EVENT_READ, handle_request)
def handle_request(client_socket, mask):
data = client_socket.recv(1024)
if data:
client_socket.send(data.upper())
else:
selector.unregister(client_socket)
client_socket.close()
server = socket.socket()
server.bind(('localhost', 8080))
server.listen()
server.setblocking(False)
selector.register(server, selectors.EVENT_READ, accept_connection)
while True:
events = selector.select()
for key, mask in events:
callback = key.data
callback(key.fileobj, mask)
触发模式对比
水平触发 (Level Triggered)
- 只要条件满足就持续触发
- Select/Poll默认模式
- 可重复检测状态
边缘触发 (Edge Triggered)
- 仅在状态变化时触发
- Epoll支持
- 需一次性处理完所有数据
性能优化建议
- 优先使用Epoll(Linux)/Kqueue(BSD)等高效机制
- 合理设置连接池大小
- 采用边缘触发提高性能
- 使用Selectors模块简化开发