IO模型介绍:html
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路复用
* signal driven IO 信号驱动IO ()
* asynchronous IO 异步IOpython
为了更好地了解IO模型,咱们须要事先回顾下:同步、异步、阻塞、非阻塞linux
同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?这个问题其实不一样的人给出的答案均可能不一样,好比wiki,就认为asynchronous IO和non-blocking IO是一个东西。这实际上是由于不一样的人的知识背景不一样,而且在讨论这个问题的时候上下文(context)也不相同。因此,为了更好的回答这个问题,我先限定一下本文的上下文。django
本文讨论的背景是Linux环境下的network IO。本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各类IO的特色和区别,若是英文够好的话,推荐直接阅读。Stevens的文风是有名的深刻浅出,因此不用担忧看不懂。本文中的流程图也是截取自参考文献。windows
Stevens在文章中一共比较了五种IO Model:
* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO多路复用
* signal driven IO 信号驱动IO
* asynchronous IO 异步IO
由signal driven IO(信号驱动IO)在实际中并不经常使用,因此主要介绍其他四种IO Model。网络
再说一下IO发生时涉及的对象和步骤。对于一个network IO (这里咱们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另外一个就是系统内核(kernel)。当一个read操做发生时,该操做会经历两个阶段:app
#1)等待数据准备 (Waiting for the data to be ready) #2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
记住这两点很重要,由于这些IO模型的区别就是在两个阶段上各有不一样的状况。框架
#同步:提交一个任务以后,要等待这个任务执行完毕 #好比去银行存钱,要取完号才能存钱 #异步:只管提交任务,不等待这个任务执行完毕,就能够作其余事情 #能够边存钱,边玩手机 #阻塞:recv recvfrom accpt 取号的时候,等待人不少 #非阻塞: #不少种状况了 #recv 等待数据准备,等待数据从内核拷贝到进程 #send 讲用户要发送的信息copy到操做系统 发送的时候的网络延迟 #主动方因此阻塞很小
#图在 有道词典 20181001 IO模型 #1.tcp/udp recv的时候—系统调用—内核操做系统数据没有准备好—等待数据------->数据准备好了,copy数据,将操做系统中的数据,复制,而且返回给进程 #两个重要节点:1.数据准备,2.将信息从内核拷贝到进程 #阻塞IO特色:程序被阻塞着,接收不了其余任务 #进程和线程,没有解决阻塞的这段时间,只是在开启多个进程/线程,该等仍是在等,每一个线程都受到了阻塞的影响 #协程只是必定程度上的解决这个问题,可是仍是在等待
#图在有道词典20181001 IO模型 #非阻塞IO #用户端: #recv ----> 告诉系统我要收数据了 ----> 系统告诉你没数据---->而后程序没有阻塞住,能够继续日后执行,可是也没收到数据 #过段时间,继续追问 #recv----> 而后系统继续告诉你没有数据,重复上面过程 #假设,一直在请求,请求了不少次,直到 #recv----> 有数据了,----> 从操做系统copy到进程中,而后告诉用户,有数据了 # 可是非阻塞IO模型毫不被推荐。 # 咱们不可否则其优势:可以在等待任务完成的时间里干其余活了(包括提交其余任务,也就是 “后台” 能够有多个任务在“”同时“”执行)。 # 可是也难掩其缺点: #1. 循环调用recv()将大幅度推高CPU占用率;这也是咱们在代码中留一句time.sleep(2)的缘由,不然在低配主机下极容易出现卡机状况 #2. 任务完成的响应延迟增大了,由于每过一段时间才去轮询一次read操做,而任务可能在两次轮询之间的任意时间完成。这会致使总体数据吞吐量的下降。 # 此外,在这个方案中recv()更多的是起到检测“操做是否完成”的做用, # 实际操做系统提供了更为高效的检测“操做是否完成“做用的接口,例如select()多路复用模式,能够一次检测多个链接是否活跃。
非阻塞IO框架,例子及解释:
import socket sk = socket.socket() sk.bind(('127.0.0.1',8088)) sk.setblocking(False) #~~~~~~~~~~~~~~将阻塞设置为不阻塞 sk.listen() conn_lst = [] del_lst = [] while True: try: conn,addr = sk.accept() print('创建了链接',addr) conn_lst.append(conn) # msg = conn.recv(1024).decode('utf8') #注释1 # print(msg) except BlockingIOError: for con in conn_lst: #循环反复的获取con try: msg = con.recv(1024) # 这里仍是非阻塞,仍是会报错 if msg == b'': # 可是若是一直从一个关闭的client获取数据,会一直打印空 del_lst.append(con) #不能 conn_lst.remove(con) 不能再for循环中列表里的元素,index会错误 continue #当con传过来为空,就不执行后面的语句了,要进行下一个循环 print(msg) con.send(b'bye') except BlockingIOError:pass for con in del_lst: con.close() conn_lst.remove(con) del_lst.clear() #注释1: #此处有两种状况,第一种因为我此时是不阻塞的(setblocking)client端以很是快速的和我聊天,致使while循环就没效果了,别人接收不了请求了 #可是若是只快速的发了一句,而后server就只能接收这一句,接下来就会由于非阻塞,而后这个conn就被内存给冲掉了 #第二种因为此时是阻塞的,client若是慢了一点给我传消息,我立刻会跳过,而且报错BlockingIOError #解决方法: #创建一个conn列表,把每次链接成功的conn,放到列表里。而且把recv放到 except语句后,由于没信息就会报错。 #而后for循环这个conn列表,一直去尝试获取,conn是否发了消息过来 #从而让原本本内存刷掉的conn,能够一直被尝试获取 #可是此是for循环的列表里,recv仍是非阻塞的,因此要继续异常处理
import time import socket import threading def func(): sk = socket.socket() sk.connect(('127.0.0.1', 8088)) sk.send(b'hello') time.sleep(0.1) print(sk.recv(1024)) sk.close() for i in range(20): threading.Thread(target=func).start()
#select 模块 #图在有道 # IO多路复用:操做系统级别的,windows的select机制,不是咱们代码提供的 # 流程: #1.有个代理(select模块),能够帮助你监听一个对象。并且这个代理能够监听多个对象 conn1,2,3,4... #能够接受数据的对象,好比 socket.accept conn.recv #发生一次系统调用, #2.而后这个代理,会替你等待 socket对象 被链接,若是没有人来连,会一直阻塞 (从而监听对象就能够不阻塞了) #3.阻塞到有client来链接,而后把 反馈信息 给帮忙监听的对象,好比accept #2-3步骤,是操做系统帮你循环监听列表,查看每一项是否有可读事件(但不是很高效) #4.而后accept知道了有数据来了,再次产生一次系统调用,找操做系统要数据 #5.系统就复制数据,传给了一开始索要数据的对象 #注意: 1~3步骤才是IO多路复用,4~5步就是 对象正常获取数据的步骤了 #监听方式: #windows 只有 select机制 #linux : # select机制: 都是操做系统轮询每个被监听的对象,看是否有读操做 #poll机制: 和select机制同样,可是poll好在能够监听的对象,比select机制多 #好比select能够监听500个,poll能够监听1000个 #随着监听项的增多,会致使监听效率下降。(好比监听列表有1000个,当我监听完第3个了,第2个来消息了,) #epoll机制: #给监听的每个对象,都绑定了一个回调函数 #每当有可读事件,回调函数就立刻进行信息反馈,而不用等待轮询 #很高效,能够在Linux上用 # selectors模块,自动帮你选择,你最适用的监听机制,根本不须要关心
IO多路复用框架,例子及解释:
import socket import select sk = socket.socket() sk.bind(('127.0.0.1',8080)) sk.setblocking(False) sk.listen() read_lst = [sk] while True: r_lst,w_lst,x_lst = select.select(read_lst,[],[]) #read_lst中的监听对象,只有有回应的,才会出如今 r_lst print('***',r_lst) # 注释1 for i in r_lst: if i is sk: conn,addr = sk.accept() read_lst.append(conn) else: ret = i.recv(1024) if ret == b'': i.close() read_lst.remove(i) # 此时 r_lst和read_lst是两个不一样内存空间,能够remove continue print(ret) i.send(b'goodbye') #注释1 #此时经历过一次链接后,read_lst原本是有着 [conn,sk], 而后此时若是有人给我发消息,也就是我此时 conn.recv了 #因为 select模块, 此时的r_lst只有 conn了,由于没有人链接我,sk也就不会出如今 r_lst里
import time import socket import threading def func(): sk = socket.socket() sk.connect(('127.0.0.1',8080)) sk.send(b'hello') time.sleep(3) print(sk.recv(1024)) sk.close() for i in range(20): threading.Thread(target=func).start()
#异步IO #步骤 #1. 阻塞对象,recv,accept,告诉系统,我要数据,而后接着就去干别的事了,不阻塞 #2.操做系统就在那边阻塞等待着数据,等到直到有数据传来了 #3.而后操做系统,直接将数据传给用户。 # (!!)可是python在这一步,没有提供这个copy data没有提供python对操做系统的接口 #因此不能用python代码实现,真正的异步IO模型 #可是c语言能够,咱们可使用不少异步框架来实现 #4.用户收到数据了。 #django就不是异步框架 #异步框架:没有waitdata和copydata的阻塞阶段,能够响应更多请求 # twisted框架 #tornado框架 # tornado 和 twisted,做为异步框架,是大同小异的。 # 只不过tornado 轻量级一些,twisted 重量级一些。在其余方面,也是互有长短。 # 通过实测,发现这两个框架,I/O性能差很少,对计算资源的占用相差较多! # 若是追求总体性能的话,推荐使用twisted。