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元,无需打电话