IO模型
IO模型
标签:并发编程
- 阻塞IO
- 非阻塞IO
- IO多路复用
- 信号驱动
- 异步IO
阻塞IO
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。
而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
只能同时一个客户端进行连接,只有一个客户端关闭另一个才会连接。
服务端
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(5)
print('starting...')
while True:
'''链接循环'''
conn, addr = server.accept() #等待连接阻塞状态
print(addr)
while True:
'''通信循环'''
try:
data = conn.recv(1024) #接收数据阻塞状态
if not data: break
conn.send(data.upper())
except Exception:
break
conn.close()
server.close()
客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8000))
while True:
msg = input('>>:').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data)
非阻塞IO
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。
也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
服务端
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8001))
server.listen(5)
server.setblocking(False)
print('starting...')
conn_l = []
del_l = []
while True:
'''链接循环'''
try:
# print(conn_l)
conn, addr = server.accept() #
conn_l.append(conn)
except BlockingIOError:
'''收不到数据的时候执行这步操作 wait data'''
for conn in conn_l:
try:
data = conn.recv(1024)
conn.send(data.upper())
except BlockingIOError:
continue
except ConnectionError:
del_l.append(conn) #for循环不能删除迭代某个数据
for obj in del_l:
conn_l.remove(obj)
del_l = []
'''缺点:CPU负载严重,2.响应时间长,每过一段时间轮询一次read操作,导致整体任务吞吐量降低'''
客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8001))
while True:
msg = input('>>:').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data)
客户端完全不变
IO多路复用
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
强调:
- 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
- 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
结论: select的优势在于可以处理多个连接,不适用于单个连接
select
服务端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8002))
server.listen(5)
server.setblocking(False)
print('starting...')
reads_l = [server,]
while True:
r_l,_,_ = select.select(reads_l,[],[]) #监测哪个socket
print(r_l)
for obj in r_l:
if obj == server:
conn,addr = obj.accept()
reads_l.append(conn)
else:
data = obj.recv(1024) #obj = conn
obj.send(data.upper())
客户端
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8002))
while True:
msg = input('>>:').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print(data)
客户端与前部分相同
poll
服务端
客户端
同步、异步、阻塞、非阻塞的区别
同步与异步
同步异步 指的是在客户端
同步意味着 客户端提出了一个请求以后,在回应之前只能等待
异步意味着 客户端提出一个请求以后,还可以继续提其他请求
阻塞非阻塞 指的是服务器端
阻塞意味着 服务器接受一个请求后,在返回结果以前不能接受其他请求
非阻塞意味着 服务器接受一个请求后,尽管没有返回结果,还是可以继续接受其他请求
首先来解释同步和异步的概念,这两个概念与消息的通知机制有关。
概念描述
所谓同步就是一个任务完成需要依赖另外一个任务时,只有等待另一个任务任务完成后,这个任务才可以完成。要么都成功要么都失败,两个任务状态可以保持一致。
所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
消息通知
异步的概念和同步相对。当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。使用哪一种通知机制,依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。
1.如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误);
2.如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。
场景比喻
举个例子,比如我去银行办理业务,可能会有两种方式:
1.选择排队等候;
2.另种选择取一个小纸条上面有我的号码,等到排到我这一号时由柜台的人通知我轮到我去办理业务了;
第一种:前者(排队等候)就是同步等待消息通知,也就是我要一直在等待银行办理业务情况;
第二种:后者(等待别人通知)就是异步等待消息通知。在异步消息处理中,等待消息通知者(在这个例子中就是等待办理业务的人)往往注册一个回调机制,在所等待的事件被触发时由触发机制(在这里是柜台的人)通过某种机制(在这里是写在小纸条上的号码,喊号)找到等待该事件的人。