1.IO模型分类
- 同步IO
-
#所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不会返回。按照这个定义,其实绝大多数函数都是同步调用。但是一般而言,我们在说同步、异步的时候,特指那些需要其他部件协作或者需要一定时间完成的任务。 #举例: #1. multiprocessing.Pool下的apply #发起同步调用后,就在原地等着任务结束,根本不考虑任务是在计算还是在io阻塞,总之就是一股脑地等任务结束 #2. concurrent.futures.ProcessPoolExecutor().submit(func,).result() #3. concurrent.futures.ThreadPoolExecutor().submit(func,).result()
- 异步IO
-
#异步的概念和同步相对。当一个异步功能调用发出后,调用者不能立刻得到结果。当该异步功能完成后,通过状态、通知或回调来通知调用者。如果异步功能用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一 种很严重的错误)。如果是使用通知的方式,效率则很高,因为异步功能几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。 #举例: #1. multiprocessing.Pool().apply_async() #发起异步调用后,并不会等待任务结束才返回,相反,会立即获取一个临时结果(并不是最终的结果,可能是封装好的一个对象)。 #2. concurrent.futures.ProcessPoolExecutor(3).submit(func,) #3. concurrent.futures.ThreadPoolExecutor(3).submit(func,)
- 阻塞IO
-
#阻塞调用是指调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 #举例: #1. 同步调用:apply一个累计1亿次的任务,该调用会一直等待,直到任务返回结果为止,但并未阻塞住(即便是被抢走cpu的执行权限,那也是处于就绪态); #2. 阻塞调用:当socket工作在阻塞模式的时候,如果没有数据的情况下调用recv函数,则当前线程就会被挂起,直到有数据为止。
- 非阻塞IO
-
#非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。
- 小结
-
#1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。 #2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程
2.阻塞IO
- blocking IO 的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了
- 解决方案:在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程)
- 上述方案存在的问题:开启多进程或多线程的方式,在遇到要同时响应成百上千的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态
- 改进方案:用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务;“连接池”维持连接的缓存池,尽量重用已有的连接,减少创建和关闭连接的频率。这两种方式都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等
- 改进方案存在的问题:“线程池”和“连接池”技术上也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”,终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以,考虑“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
- 总结:多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
3.非阻塞IO
-
#-*- coding:utf-8 -*- from socket import * ip = gethostbyname(gethostname()) server = socket(AF_INET,SOCK_STREAM) server.bind((ip,8080)) server.listen(5) server.setblocking(False)#非阻塞IO rlist = [] wlist = [] while True: try: conn,addr = server.accept() rlist.append(conn) print(rlist) except BlockingIOError: del_rlist = [] for sock in rlist: try: data = sock.recv(1024) if not data: del_rlist.append(sock) else: wlist.append((sock,data.upper())) except BlockingIOError: continue except Exception: sock.close() del_rlist.append(sock) del_wlist = [] for item in wlist: try: sock = item[0] data = item[1] sock.send(data) del_wlist.append(item) except BlockingIOError: pass for item in del_wlist: wlist.remove(item) for sock in del_rlist: rlist.remove(sock) server.close()
-
1 #-*- coding:utf-8 -*- 2 import socket 3 4 server_ip = socket.gethostbyname(socket.gethostname()) 5 client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 6 client.connect((server_ip,8080)) 7 while True: 8 msg = input('>>: ') 9 if not msg: continue 10 client.send(msg.encode('utf-8')) 11 data = client.recv(1024) 12 print(data.decode('utf-8'))
- 优点:能够在等待任务完成的时间里干其他的活(包括提交其他任务,也就是“后台”可以有多个任务在“同时”进行)
- 缺点:
- 1.循环调用recv()将大幅度推高CPU占用率,这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
- 2.任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能再两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低
- 解决方案:select()多路复用模式,可以一次检测多个连接是否活跃
4.多路复用IO
- select/epoll的好处在于单个process就可以同时处理多个网络连接的IO。select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
- 强调
- 1.如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接
- 2.在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block
- 3.结论: select的优势在于可以处理多个连接,不适用于单个连接
-
1 # -*- coding:utf-8 -*- 2 from socket import * 3 import select 4 5 ip = gethostbyname(gethostname()) 6 server = socket(AF_INET, SOCK_STREAM) 7 server.bind((ip, 8080)) 8 server.listen(5) 9 server.setblocking(False) # 非阻塞IO 10 print("starting...") 11 12 rlist = [server, ] 13 wlist = [] 14 wdata = {} 15 16 while True: 17 rl, wl, xl = select.select(rlist, wlist, [], 0.5) 18 print(wl) 19 for sock in rl: 20 if sock == server: 21 conn,addr = sock.accept() 22 rlist.append(conn) 23 else: 24 try: 25 data = sock.recv(1024) 26 if not data: 27 sock.close() 28 rlist.remove(sock) 29 continue 30 wlist.append(sock) 31 wdata[sock]=data.upper() 32 except Exception: 33 sock.close() 34 rlist.remove(sock) 35 for sock in wl: 36 sock.send(wdata[sock]) 37 wlist.remove(sock) 38 wdata.pop(sock)
- 优点:相比其他模型,使用select()的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多CPU,同时能够为多客户端提供服务,如果试图建立一个简单的服务器程序,这个模型有一定的参考价值
- 缺点:
-
首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。 很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。 如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异, 所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
-