1.I/O模型中的同步-异步,阻塞-非阻塞
"同步":调用发出后,不会立即返回结果,一旦返回结果,就是最终结果
"异步":调用者发出请求后,被调用方立刻返回消息,但返回的消息并不是最终结果。
得到返回消息,调用者可以处理其他事情。
被调用方处理完成,得到最终结果,会发送信号给调用方或通过回调函数来进行处理
"阻塞":调用结果返回前,调用者会被挂起。调用者只有在得到返回结果后才能继续
"非阻塞":即使数据没准备好,调用者也会立刻得到一个返回消息
得到返回消息后,可以做其他事情
之后调用者需要不断询问被调用方,被调用方每次都会返回一个消息
直到数据有了,在调用者询问时,被调用者才返回真正需要的数据
1.1阻塞IO
在linux中,默认情况下都是阻塞IO
工作流程:
当用户执行recv(),如果服务端不send()数据,客户端就一直等在那里
直到服务端send()数据,通过网络发送到客户端服务器,再从内核copy到用户程序
"blcok IO的特点
在IO执行的两个阶段(等待数据和拷贝数据)都阻塞了
在阻塞阶段,不能处理其他用户请求
可通过多进程/多线程实现同时处理多个用户请求"
对于可通过多进程/多线程的方式实现并发
"该方案问题"
开启多进程或多线程,如果要同时响应成百上千的连接请求,会严重占据系统资源
降低系统的响应效率,而且线程与进程本身容易进入假死状态
"改进方案"
可能会考虑"线程池或连接池"
线程池目的是减少创建和销毁线程的频率,维持合理数量的线程,并让空闲的线程重新执行新的任务
连接池是维持连接的缓存池,尽量重用已有的连接,减少创建和关闭的系统开销。
这两种技术可以很好的降低系统开销,被广泛使用与tomcat,webserver,和各种数据库中
"改进方案的问题"
线程池或连接池只在一定程度上缓解了频繁调用IO带来的资源占用
线程池和连接池是有连接数限制的
如果是上万次的客户请求,或许可以缓解部分压力,但是对于大规模的气你去,会遇到瓶颈
可以用非阻塞IO尝试解决问题
1.2非阻塞IO
工作流程
当用户发出recv(),如果kernel没有数据准备好,不会阻塞用户程序,而是立刻返回一个error
用户立刻得到返回结果,当用户判断是一个error时,直到数据没有准备好,可以做些其他事情
用户后面会循环多次发出recv(),多次得到error
直到kernel中的数据准备好了,并且再次收到了用户的recv(),就立刻把数据copy到用户内存
"非阻塞IO特点
可以处理多个用户请求
用户需要不断询问kernel数据准备好没有
循环recv()将大幅度占用CPU,低配主机可能出现卡机"
此方案中的循环recv()主要是监测数据是否准备好,实际操作系统提供了select()多路复用模式,可以一次监测多个连接
"由于是不断recv,所以会发现笔记本的风扇声音越来越大,因为电脑CPU繁忙"
#服务端
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8099))
server.listen(5)
server.setblocking(False)
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)
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()
#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8099))
while True:
msg=input('>>: ')
if not msg:continue
c.send(msg.encode('utf-8'))
data=c.recv(1024)
print(data.decode('utf-8'))
1.3多路复用IO
多路复用IO,就是select/epoll,也叫"事件驱动IO"
工作流程
当用户调用了select,整个进程会被block。同时,select/epoll这个function会不断询问所负责的所有socket
当任何一个socket中的数据准备好了,select就会返回
这个时候用户调用recv(),将数据从kernel拷贝到用户进程
"select特点
select与阻塞IO图差不多,主要特点在于可以同时处理多个connection
多路复用IO是阻塞在中间调度者,即阻塞在select中,所以可以同时处理多个用户请求"
强调:
1.如果处理的连接数不是很高,使用select/epoll的web server不一定比使用多线程+阻塞IO的webserver性能更好,可能延迟更大。
2.select/epoll的优势不是在于单个连接处理的更快,而是在于能够处理更多的连接
"select帮助管理多个socket,每隔0.5秒询问数据是否准备好"
#服务端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8093))
server.listen(5)
server.setblocking(False)
print('starting...')
rlist=[server,]
wlist=[]
wdata={}
while True:
rl,wl,xl=select.select(rlist,wlist,[],0.5)
print(wl)
for sock in rl:
if sock == server:
conn,addr=sock.accept()
rlist.append(conn)
else:
try:
data=sock.recv(1024)
if not data:
sock.close()
rlist.remove(sock)
continue
wlist.append(sock)
wdata[sock]=data.upper()
except Exception:
sock.close()
rlist.remove(sock)
for sock in wl:
sock.send(wdata[sock])
wlist.remove(sock)
wdata.pop(sock)
#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8093))
while True:
msg=input('>>: ')
if not msg:continue
c.send(msg.encode('utf-8'))
data=c.recv(1024)
print(data.decode('utf-8'))
1.4异步IO
工作流程
用户发起recv()后,立刻得到返回,不会被block
此时可以做其他事情
当kernel等待的数据到达,会把数据从内核拷贝到用户内存,然后发给用户一个信号,告诉它recv操作完成了
1.5总结
摘自网友https://www.jianshu.com/p/6a6845464770的书写,感觉很生动形象
1.阻塞I/O模型
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。
2.非阻塞I/O模型
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
耗费:往返车站6次,路上6小时,其他时间做了好多事。
3.I/O复用模型
1.select/poll
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
2.epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
4.异步I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话