IO模型(多路复用IO)
IO模型
I/O(input/output):输入输出
IO所存在的问题:当我们要输入数据或是要输出数据时,通常需要很长一段时间(对于CPU而言)
而在等待的过程中,CPU就处于闲置状态,造成了资源的浪费
注意:IO其实有很多类型,例如:socket网络IO,内存到内存的copy,等待键盘输入。而这些对比起来socket网络IO需要等待的时间是最长的,也是我们重点关注的地方
而IO模型,就是要让我们在等待IO操作的过程中利用CPU,做别的事情
网络IO经历的步骤和过程
操作系统的两种状态:内核态和用户态。
当操作系统需要控制硬件时,例如接收网卡上的数据,必须先转到内核态,接受完数据后,要把数据从操作系统缓存区,copy到应用程序的缓冲区,也就是从内核态转为用户态。
接收数据的过程(如accept,recv):
- 等待数据到达网卡,叫做wait data
- 从内核copy到应用程序缓冲区,叫做copy data
发送数据的过程(如send):
- 数据从应用程序缓冲区copy到内核
阻塞IO模型
默认情况下,写出的TCP程序就是阻塞IO模型
该模型,提高效率方式:当你执行recv/accept,会进行wait data阶段。
- 你的进程会主动调用一个block指令,进程进入阻塞状态,同时让出CPU的执行权,操作系统就会将CPU分配给其他任务,从而提高了CPU的利用率
- 当数据到达时,首先会从内核将数据copy到应用程序缓冲区,并且socket将唤醒处于自身等待队列中的所有进程
之前使用多进程,多线程完成的并发,其实都是阻塞IO模型,每个线程在执行recv时,也会卡住
非阻塞IO模型
与阻塞IO模型相反,在调用recv,accept时都不会阻塞当前线程,
使用方法:将原本阻塞的socket设置为非阻塞
from socket import *
import time
server = socket()
server.bind(('127.0.0.1', 8000))
server.setblocking(False) # 将阻塞变成非阻塞
server.listen(5)
cs = [] # 用来存放已连接的客户端
msgs = [] # 用来存放客户端发来的数据
while True:
time.sleep(0.1) # 时间长了,客户端那边会有延迟,时间短了,对CPU的占用又会很大
try:
conn, client_addr = server.accept()
print(client_addr)
cs.append(conn)
except BlockingIOError:
print('没有人')
# 代码执行到这里说明没有连接需要处理
# 那就处理收发数据的任务
for conn in cs[:]: # 用来接收数据
try:
data = conn.recv(1024)
if not data: raise ConnectionResetError
print(data.decode('utf-8'))
msgs.append((conn, data.upper()))
except BlockingIOError:
print('没有接受数据')
except ConnectionResetError:
conn.close()
cs.remove(conn)
for msg in msgs[:]: # 用来发送数据
try:
msg[0].send(msg[1])
msgs.remove(msg)
except BlockingIOError:
pass
except ConnectionResetError:
msg[0].close()
msgs.remove(msg)
cs.remove(msg[0])
该模型在没有数据到达时,会报异常,而我们需要捕获异常,然后继续不断地询问系统内核,直到数据到达为止(有点像轮询)
可以看出,该模型会大量的占用CPU的资源去做一些无效的循环,效率低于阻塞IO模型。
报错的类型
- 服务端接受不到数据(accept,recv)的时候,会报错BlockingIOError
- 客户端正常退出,服务端会一直接受空
- 客户端断开了,再执行recv会报错ConnectionResetError
- 服务器不停地发送数据,就会把缓冲区塞满,然后报错BlockingIOError
- 客户端断开了,再执行send会报错ConnectionResetError
多路复用IO模型(重要)
属于事件驱动
多个socket使用同一套处理逻辑
以点餐为例:
非阻塞IO,相当于你每次去问,照着菜单挨个问好了没
多路复用则是你直接和前台说好你有哪些菜,然后直接问前台哪些菜做好了,前台则会给你返回一个列表,里面是已经做好的菜。
from socket import *
import select
server = socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
print('wait...')
r_list = [server]
w_list = []
msgs = {}
while True:
# 如果所有盘子里都没有东西,就会阻塞在这里
read_able, write_able, _ = select.select(r_list, w_list, [])
# 处理可读的盘子,也就是接收数据
for obj in read_able: # 遍历所有可读数据的socket
# 有可能是服务器,有可能是客户端
if obj == server:
conn, client_addr = server.accept()
print(client_addr,'连接成功')
r_list.append(conn)
else:
try:
data = obj.recv(1024)
if not data: raise ConnectionResetError
print('收到一条新消息', data.decode('utf-8'))
# 将要发送数据的socket加入到可写的盘子里让select检测
w_list.append(obj)
# 把要发送的数据以{对象:[数据,数据]}的格式放入容器
if msgs.get(obj):
msgs[obj].append(data)
else:
msgs[obj] = [data,]
except ConnectionResetError:
print('连接已断开')
obj.close()
r_list.remove(obj)
msgs.pop(obj) #todo
# 处理可写的盘子,也就是send数据
for obj in write_able:
msg_list = msgs.get(obj)
if msg_list:
# 遍历发送所有的数据
for i in msg_list:
try:
obj.send(i)
except ConnectionResetError:
msgs.pop(obj)
w_list.remove(obj)
break
# 把数据从容器中删除
msgs.pop(obj)
# 把这个socket从w_list中删除
w_list.remove(obj)
对比之前提到的阻塞或是非阻塞IO模型,增加了一个select,来帮我们检测socket的状态,从而避免了我们自己去检测socket带来的开销
select有四个盘子,是否可读,是否可写,可读,可写,异常的先不管,自己处理异常
我们只要将任务放入是否可读可写的盘子中,那么一旦任务就绪了,就会返回到可读可写的盘子中,而我们则需要遍历列表,再分别处理读写即可。
多路复用对比非阻塞
优势:多路复用可以极大降低CPU的占用率。
弊端:多路复用本质,多个任务之间是串行的,如果某个任务的耗时较长,将导致其他的任务无法立即执行。
所以多路复用的最大优点就是高并发,但是能够处理的数据不会太大(如果需要处理的数据量大,也可以一部分一部分的处理)。
异步IO模型
异步IO != 非阻塞IO
因为非阻塞IO在copy data的过程是一个同步的过程,会卡住主线程,只是比较快
而异步IO,是发起任务后,就可以继续执行其他任务,只有当数据已经拷贝到应用程序缓存区,才会给你的线程发送信号,或者执行回调
asyncio
信号驱动IO模型
简单地说,就是当某个事情发送后,会给你的线程发送一个信号,那么你的线程就可以去处理这个任务了
不常用的原因是,socket的信号太多了,处理起来非常繁琐