十九、并发编程之IO模型
十九、并发编程之IO模型
一、IO模型
1.网络IO的两个重要阶段:
1.等待数据准备:waitdata
2.将数据从内核拷贝到进程中:copydata
记住这两点很重要,因为这些IO模型的区别就是在两个阶段上各有不同的情况
补充:
1 #1.输入操作:read,readv,recv,recvmsg共5个函数,如果会阻塞状态,则会经历waitdata和copydata两个阶段,如果设置为非阻塞则在wait 不到data时抛出异常 2 3 #2.输出操作:write,writev,send,sendmsg共5个函数,在发送缓冲区满了会阻塞在原地,如果设置为非阻塞,则会抛出异常 4 5 #3.接受外来链接:accept与输入操作类似 6 7 #4.发起外出链接:connect与输出操作类似
二、阻塞IO
在linux中,默认情况下所有的socket都是blocking
blocking IO的特点就是在IO执行的两个阶段都被block了
实际上,除非特别指定,几乎所有的IO接口(包括socket接口)都是阻塞型的,这给网络编程带来了一个很大的问题,如在调用recv(1024)的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求
一个简单的解决方案:
1 多线程和多进程,目的是让每个接连拥有独立的线程或进程,这样任何一个连接的阻塞都不会影响到其他的连接
该方案的问题是:
1 开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。
改进方案:
1 很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。
改进后方案其实也存在着问题:
1 “线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
总结:
1 对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
三、非阻塞IO
可以通过设置socket使其变为non-blocking,也就是非阻塞形式
在非阻塞IO中,用户进程其实是需要不断的主动询问操作系统数据准备好了没有
1 # 服务端 2 import socket 3 import time 4 5 server=socket.socket() 6 server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 7 server.bind(('127.0.0.1',8083)) 8 server.listen(5) 9 10 server.setblocking(False) 11 r_list=[] 12 w_list={} 13 14 while 1: 15 try: 16 conn,addr=server.accept() 17 r_list.append(conn) 18 except BlockingIOError: 19 # 强调强调强调:!!!非阻塞IO的精髓在于完全没有阻塞!!! 20 # time.sleep(0.5) # 打开该行注释纯属为了方便查看效果 21 print('在做其他的事情') 22 print('rlist: ',len(r_list)) 23 print('wlist: ',len(w_list)) 24 25 26 # 遍历读列表,依次取出套接字读取内容 27 del_rlist=[] 28 for conn in r_list: 29 try: 30 data=conn.recv(1024) 31 if not data: 32 conn.close() 33 del_rlist.append(conn) 34 continue 35 w_list[conn]=data.upper() 36 except BlockingIOError: # 没有收成功,则继续检索下一个套接字的接收 37 continue 38 except ConnectionResetError: # 当前套接字出异常,则关闭,然后加入删除列表,等待被清除 39 conn.close() 40 del_rlist.append(conn) 41 42 43 # 遍历写列表,依次取出套接字发送内容 44 del_wlist=[] 45 for conn,data in w_list.items(): 46 try: 47 conn.send(data) 48 del_wlist.append(conn) 49 except BlockingIOError: 50 continue 51 52 53 # 清理无用的套接字,无需再监听它们的IO操作 54 for conn in del_rlist: 55 r_list.remove(conn) 56 57 for conn in del_wlist: 58 w_list.pop(conn) 59 60 61 62 #客户端 63 import socket 64 import os 65 66 client=socket.socket() 67 client.connect(('127.0.0.1',8083)) 68 69 while 1: 70 res=('%s hello' %os.getpid()).encode('utf-8') 71 client.send(res) 72 data=client.recv(1024) 73 74 print(data.decode('utf-8'))
但是非阻塞IO模型绝不被推荐使用
我们不能否认其优点:能够在等待任务完成的时间里干其他活了
但是也难掩盖其缺点:循环调用recv()将大幅度提高CPU的占用率,任务完成的相应延迟时间也增大了
四、多路复用IO
select的优势在于可以处理多个连接,不适用于单个连接
1 #具体的使用方法: 2 将所以有连接交给select管理,判断哪些连接可以被处理 3 作为主线程,不需要重复不断的问操作系统拿数据,而是等待select返回需要的处理数据,等待意味着select是阻塞的 4 5 一、创建连接和管理连接 6 1.创建服务器socket对象 7 2.将服务器对象交给select来管理 8 3.一旦有客户端发起连接select将不再阻塞 9 4.select将返回一个可读的socket对象(第一次只有服务器对象) 10 5.服务器的可读代表有连接请求,需要执行accept,返回一个客户端连接coon,由于是非阻塞的,不能立即去recv 11 6.把客服端socket对象也交给select来管理,将conn加入两个被检测的列表中 12 7.下一次检测到可读的socket,可能是服务器,也可能是客户端,所以加上判断服务器就accpet,客户端就recv 13 8.如果检测到有可写(可以send就是系统缓存可用)的socket对象,则说明可以向客户端发送数据了 14 15 二、处理数据收发 16 两个需要捕获异常的地方 17 1.recv执行第7步时,表示可以读,如果客户端断开连接就会抛出异常,在Linux下还需要加上if not 判断是否有数据,因为在Linux下客户端断开不会抛出异常,会循环收到空信息 18 2.send执行第8步,表示可以写,如果客户端断开连接,就会抛出异常
1 import socket 2 import select 3 4 server=socket.socket() 5 #重用端口 6 server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 7 server.bind(('127.0.0.1',10001)) 8 server.listen(5) 9 #设置socket为非阻塞 10 server.setblocking(False) 11 12 #创建检测列表 13 rlist=[server,] 14 wlist=[] 15 dic={} 16 17 while True: 18 # 返回检测后的列表 19 rl, wl, xl = select.select(rlist, wlist, []) 20 for c in rl: 21 if c ==server: 22 c,addr=c.accept() 23 rlist.append(c) 24 else: 25 try: 26 data=c.recv(1024) 27 print(data.decode('utf-8')) 28 wlist.append(c) 29 dic[c]=data 30 except ConnectionResetError: 31 c.close() 32 wlist.remove(c) 33 dic.pop(c) 34 for c in wl: 35 try: 36 c.send(dic[c].upper()) 37 dic.pop(c) 38 wlist.remove(c) 39 except ConnectionResetError: 40 rlist.remove(c) 41 wlist.remove(c) 42 dic.pop(c)
该模型的优点:
1 #相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
该模型的缺点:
1 #首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 2 #其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
五、异步IO
1、IO包括:网络IO,本地IO
本地IO的解决方案:将同步的IO操作改成异步的IO操作,在IO期间可以执行其他任务
使用asyncio模块aiohttp模块
import asyncio import aiohttp import sys host = 'http://www.baidu.com' loop = asyncio.get_event_loop() async def fetch(url): async with aiohttp.ClientSession(loop=loop) as session: async with session.get(url) as response: response = await response.read() # print(response) return response if __name__ == '__main__': import time start = time.time() tasks = [fetch(host) for i in range(int(sys.argv[1]))] loop.run_until_complete(asyncio.gather(*tasks)) print("spend time : %s" %(time.time()-start))