一 .IO模型
1.IO模型简介
https://www.cnblogs.com/nickchen121/p/11145098.html
https://www.cnblogs.com/Eva-J/articles/8324837.html
同步(synchronous) I/O和异步(asynchronous) I/O,阻塞(blocking) I/O和非阻塞(non-blocking)I/O分别是什么,到底有什么区别?这个问题其
实不同的人给出的答案都可能不同,
比如wiki,就认为asynchronous I/O和non-blocking I/O是一个东西。这其实是因为不同的人的知识背景不同,并且在讨论这个问题的时候上下文(context)也不相同。所以,为了更好的回答这个问题,
我先限定一下本文的上下文。
住这两点很重要,因为这些I/O模型的区别就是在两个阶段上各有不同的情况。 在网络环境下,再通俗的讲,将I/O分为两步: 等;
数据搬迁。
如果要想提高I/O效率,需要将等的时间降低。 五种I/O模型包括:阻塞I/O、非阻塞I/O、信号驱动I/O、I/O多路转接、异步I/O。其中,前四个被称为同步I/O。
2. 阻塞IO模型
通俗理解阻塞Io模型 以买票的例子举例,该模型小结为
# 老王去火车站买票,排队三天买到一张退票。
# 耗费:在车站吃喝拉撒睡 3天,其他事一件没干
阻塞IO模型图
当用户进程调用了recvfrom这个系统调用,kernel就开始了I/O的第一个阶段:准备数据。对于network I/O来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包)
这个时候kernel就要等待足够的数据到来。 而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,
重新运行起来。所以,
blocking I/O的特点就是在I/O执行的两个阶段
(等待数据和拷贝数据两个阶段)都被block了。
几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv() 等接口开始的,使用这些接口可以很方便的构建服务器/客户机的模型。然而大部分的socket接口都是阻塞型的。如下图 所谓阻塞型接口是指系统调用(一般是I/O接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。
1.简单的解决方案 (这个方法有一些弊端)
在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接
改进后方案的问题
1. 很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。
这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。
2.“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用I/O接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界
的响应并不比没有池的时候效果好多少所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
3. 对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,
多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
3. 非阻塞IO模型
通俗理解非阻塞IO模型 以买票的例子举例,该模型小结为:
# 老王去火车站买票,隔12小时去火车站问有没有退票,三天(72个小时)后买到一张票。
# 耗费:往返车站6次,路上6小时,其他时间做了好多事。
非阻塞Io模型图
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,
而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。
一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。 也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,
可以干点别的事情,然后再发起recvform系统调用。
重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。
需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态
。所以,在非阻塞式I/O中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有
#服务端 from socket import * import time s=socket(AF_INET,SOCK_STREAM) s.bind(('127.0.0.1',8080)) s.listen(5) s.setblocking(False) #设置socket的接口为非阻塞 conn_l=[] del_l=[] while True: try: conn,addr=s.accept() conn_l.append(conn) except BlockingIOError: print(conn_l) for conn in conn_l: try: data=conn.recv(1024) if not data: del_l.append(conn) continue conn.send(data.upper()) except BlockingIOError: pass except ConnectionResetError: del_l.append(conn) for conn in del_l: conn_l.remove(conn) conn.close() del_l=[] #客户端 from socket import * c=socket(AF_INET,SOCK_STREAM) c.connect(('127.0.0.1',8080)) while True: msg=input('>>: ') if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8')) 非阻塞IO实例
但是非阻塞I/O模型绝不被推荐。
我们不能否则其优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“”同时“”执行)。 但是也难掩其缺点:
循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。
4.多路复用IO模型
I/O多路复用,I/O就是指的我们网络I/O,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。
串起来理解就是很多个网络I/O复用一个或少量的线程来处理这些连接。
作用就是检测多个seckot是否发生了变化
非阻塞(setblocking(False))+io多路复用
通俗理解多路复用阻塞IO模型 (select/poll模型) 以买票的例子举例,该模型小结为:
# 老王去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老王去火车站交钱领票。 # 耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
多路复用模型图
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。
这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking I/O的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking I/O只调用了一个系统调用(recvfrom)。
但是,用select的优势在于它可以同时处理多个connectI/On。
如果处理的连接数不是很高的话,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好,可能延迟还更大。
select/poll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。
只不过process是被select这个函数block,而不是被socket I/O给block。 结论: select的优势在于可以处理多个连接,不适用于单个连接
单线程并发
# 单线程 + 非阻塞 =IO多路复用
# 说别了就是利用IO等待 做别的事件
IO多路复用 检测多个socket是否发生了变化 (是否连接成功 / 是否已经获取数据 ) 可读/可写
import socket import select client1=socket.socket() client1.setblocking(False) # 默认是阻塞 开启非阻塞 try: client1.connect(("www.baidu.com",8700)) except BlockingIOError as e: pass client2=socket.socket() client2.setblocking(False) # 默认是阻塞 开启非阻塞 try: client2.connect(("www.sogou.com",8700)) except BlockingIOError as e: pass client3=socket.socket() client3.setblocking(False) # 默认是阻塞 开启非阻塞 try: client3.connect(("www.oldboyedu.com",8700)) except BlockingIOError as e: pass socket_list=[client1,client2,client3] conn_list=[client1,client2,client3] while True: rlist,wlist,elist=select.select(socket_list,conn_list,[],0.005) # 检测这几个socket发生了变化 # wlist 中表示连接成功的socket对象 sk表示[client1 ,client2,client3] for sk in wlist: if sk==client1: sk.sendall(b'GET /s?wd=lover HTTP/1.0 host:www.baidu.com ') elif sk==client2: sk.sendall(b'GET /web?query=aa HTTP/1.0 host:www.sogou.com ') elif sk==client3: sk.sendall(b'GET /s?wd=lover HTTP/1.0 host:www.oldboyedu.com ') conn_list.remove(sk) # 表示上面三个连接谁连接成功了就移除掉 # rlist 是表示上面发送成功了的数据 返回 回来的结果 就是接收 for sk in rlist: chunk_list=[] while True: try: # 没有阻塞 如果没有数据回来(接收)就会报错 所以做个异常处理 chunk=sk.recv(8096) if not chunk: break chunk_list.append(chunk) except BlockingIOError as e: break body=b''.join(chunk_list) print(body)# 表示 数据回来了 接收成功了 # sk.close() # 关闭连接 socket_list.remove(sk) # 移除连接 if not socket_list: # 表三个数据都回来了 就是接收完了 break
select 参数分析
4.1.3 该模型的优点 相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,
这个模型有一定的参考价值。 4.1.4 该模型的缺点 首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。 很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。 如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异, 所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
5. 异步IO模型
通俗理解异步阻塞IO模型 以买票的例子举例,该模型小结为:
# 老王去火车站买票,给售票员留下电话,有票后,售票员电话通知老王并快递送票上门。
# 耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话
异步IO模型图
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。
然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。