并发编程 - IO模型
1 IO模型简介
前提,这里介绍的IO模型针对的是网络IO。
1.1 五种IO模型
IO模型 | IO Model |
---|---|
阻塞IO | Blocking IO |
非阻塞IO | Nonblocking IO |
IO多路复用 | IO Multiplexing |
信号驱动IO | Sigal Driven IO |
异步IO | Asychronous IO |
1.2 网络通信过程简介
copy data阶段
发送数据:程序将待发送的数据进行拷贝并交给操作系统。
接收数据:操作系统将待接收的数据进行拷贝并交给指定的程序。
这里以接收方为例。
阶段1 等待数据准备
接收方一直等待发送方发送数据
阶段2 将数据从内核拷贝到进程中
接收方收到数据后将数据经由操作系统拷贝给指定的程序(copy data阶段)
1.3 常见的网络阻塞状态
accept
recv/recvfrom
上面两个动作属于被动执行,即是否执行由对方决定,如果对方发送连接请求/数据,则执行accept/recv/recvfrom,否则一直处于等待状态。
send也属于IO操作,但属于主动触发,即发送数据的动作由自己决定,因此造成阻塞的时间非常短,不在这里讨论由send引发的阻塞。
2 阻塞IO模型
- 程序使用recv(TCP) / recvfrom(UDP) 发送系统调用,向内核(操作系统)索要数据,操作系统等待对方发送数据;
- 操作系统收到数据后,将数据进行拷贝给程序。
上面两个阶段执行结束后程序才能继续执行。
3 非阻塞IO模型
程序每次向操作系统发送系统调用后,都会立即得到响应,无论是否存在数据,这意味着程序发送系统调用后不会等待,可以立即处理其它事情。
非阻塞IO模型将阻塞IO模型中的所有阻塞操作变成非阻塞操作,程序一直处于就绪态和运行态,因此程序可以尽可能地占用CPU的资源。
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
# 将所有的网络阻塞变为非阻塞 (accept, recv)
# 这样做在没有对方回应时,accept, recv会抛出BlockingIOError异常。
server.setblocking(False)
r_list = []
del_list = []
while True:
try:
# 判断是否收到新的连接请求
# 没有则抛出异常
conn, addr = server.accept()
r_list.append(conn)
except BlockingIOError:
# 说明目前没有新的客户端连接请求
# 检查已连接的客户端是否发送数据
for conn in r_list:
try:
# 判断是否接收到来自客户端的数据
# 没有则抛出异常
data = conn.recv(1024)
# 说明客户端断开了连接(Linux)
if len(data) == 0:
conn.close()
del_list.append(conn)
continue
data = data.decode('utf-8')
conn.send(data.upper().encode('utf-8'))
except BlockingIOError:
# 说明这个客户端连接没有发送数据,判断下一个
continue
except ConnectionResetError:
# 说明客户端断开了连接(Windows)
conn.close()
del_list.append(conn)
# 回收失效的连接
for conn in del_list:
r_list.remove(conn)
del_list.clear()
总结:
非阻塞IO模型会导致程序长时间占用CPU却不做任何实际的任务,因此该模型非常消耗和浪费CPU资源,实际应用中不会考虑使用非阻塞IO模型。
4 IO多路复用模型
操作系统提供监管机制,用来监听socket对象和conn对象,当对象被触发时立即返回对应的对象。
当监管的对象只有一个时,IO多路复用模型的效率低于阻塞IO模型的效率,但是IO多路复用模型可以一次性监管大量的对象。
注意,这个监管机制是操作系统提供的。
import socket
import select
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
server.setblocking(False)
read_list = [server]
while True:
# rlist, wlist, xlist
r_list, w_list, x_list = select.select(read_list, [], [])
# 监测server对象
# 当收到客户端的连接请求后,建立socket对象(server)。
# socket对象建立完毕后处于可读的状态,操作系统会将socket对象返回。
for each_obj in r_list:
# r_list存在socket对象和处理连接请求后添加的conn对象
# 需要判断不同类型的对象
if each_obj is server:
conn, addr = each_obj.accept()
read_list.append(conn)
else:
try:
data = each_obj.recv(1024)
if len(data) == 0:
each_obj.close()
read_list.remove(each_obj)
continue
data = data.decode('utf-8')
print(data)
each_obj.send(data.upper().encode('utf-8'))
except ConnectionResetError:
each_obj.close()
read_list.remove(each_obj)
监管机制汇总
- select机制
windows和Linux - poll机制
Linux
poll机制监管的总量比select机制监管的总量多。
当需要监管的对象非常多时,poll机制和select机制可能出现非常大的延时响应。 - epoll机制
Linux
为每一个监管对象绑定了一个回调机制,一旦出现响应,可以通过回调机制立即处理。
5 异步IO模型
程序发送系统调用后不会等待,立即去处理其它事情。
操作系统处理完数据后会向程序发送信号,程序收到信号后停下正在进行的任务,去接收并处理操作系统发送的数据。
异步IO模型可以在单线程下实现并发的效果。
异步IO模型是所有模型中效率最高,也是使用最广泛的。
模块:asyncio
支持异步的框架:sanic, tronado, twisted
支持异步的框架的特点是运行速度非常快。
import asyncio
import threading
@asyncio.coroutine
def print_hello():
print(f'Hello. {threading.current_thread()}')
yield from asyncio.sleep(1) # 模拟IO操作
print(f'Hello. {threading.current_thread()}')
loop = asyncio.get_event_loop()
task_list = [print_hello(), print_hello()]
loop.run_until_complete(asyncio.wait(task_list))
loop.close()