第七章|7.4并发编程| I/O模型

I/O模型

协程是单线程下的并发,并不是对性能都有所提升,一定是监测单个线程下的多个任务的I/O,遇到I/O不要让它阻塞,给它自动切换到其他任务去,这样就能提高单个线程下的运行效率。--->>用gevent模块来实现了,gevent是怎么检测I/O行为的呢,gevent监测行为,遇到I/O自动切换到其他任务去,并发效果有了性能也提升了。

讲I/O模型就是为了你自己去实现一个gevent模式。

同步异步(提交任务的方式),同步调用是提交完任务在原地等着结果拿到结果后再运行下行代码;异步调用是提交完就不管了接着往下执行,异步通常跟回调机制连用,我提交完一个任务,这个任务运行完后会自动触发回调函数运行把结果交给它。

同步不等于阻塞。同步只是提交任务的方式,任务是否阻塞我不管,我就在这等着只要运行完了才往下走;阻塞是遇到I/O了,你自己没有处理,操作系统会剥夺你使用cpu给别人用。我们要解决的就是一个线程下可能要干的活很多,你不要遇到I/O就卡在原地了,我能够检测到它确实是一个I/O行为好切换到其他任务;非阻塞和阻塞正好相反。

遇到I/O为什么会卡那呢,我们要研究的是套接字I/O行为,实现服务端尽可能多的支持并发、性能提升起来,研究的主要是网络I/O。

服务端套接字accept(等着别人连接,给你发一个数据,会让你感觉明显的等),recv(收消息) ,send(传消息,并不会让你感觉等)都属于I/O阻塞行为;

recv经历两个阶段:waite等数据(从自己的缓存数据收消息,从客户端到服务端,耗时最长);从操作系统缓存内存copy到应用程序内存;accept和recv是一个的都要经历这两个阶段。send是应用程序把自己的数据copy到操作系统内存,本地copy的速度非常快,它只经历这一个阶段,然后接下来由操作系统去干。

四种I/O模型不同之处就是对这两个阶段的处理不一样。

阻塞I/O

扫描二维码关注公众号,回复: 4198464 查看本文章

recvfrom要经历wait for data和copy data 两个阶段才算执行完毕,这就就是阻塞I/O。

#阻塞I/O  没有并发、遇到阻塞就等着
from socket import *
from threading import Thread #可利用多线程实现并发,让主线程干accept的活
def communicate(conn):
    while True:   #通信循环
        try:
            data = conn.recv(1024) #等待,等着收消息,客户端要产生数据
            if not data: break
            conn.send(data.upper())
        except ConnectionResetError:
            break

    conn.close()

server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

while True: #连接循环
    print('starting...')
    conn, addr = server.accept()  #wait阶段主要卡在这里;accept等着对方连我,别人给我发消息才算过去了
    print(addr)#客户端来了

    t=Thread(target=communicate,args=(conn,)) #让主线程干accept的活,每来一个链接发起个线程让它干通信的活;没有解决I/O阻塞,起了多个线程各个运行互不影响;
    t.start()                                 #线程池保证机器在健康的状态下运行;

server.close()
View Code
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080)) #发连接

while True:
    msg=input('>>: ').strip() #输消息
    if not msg:continue
    client.send(msg.encode('utf-8')) #发消息
    data=client.recv(1024)
    print(data.decode('utf-8')) #收消息
client.close()
View Code

非阻塞I/O

单线程下的多并发

监测单线程下的I/O,帮你自动切换到另外一个任务去运行,wait和copy阶段是怎么处理的呢,跟gevent一样。

由应用程序发起个系统调用recvfrom打算收一个消息,发给操作系统内核,在阻塞I/O它会让你一直在原地等着。非阻塞I/O,发给操作系统内核,操作系统会说没有数据准备好,返回个信号给你的应用程序,应用程序就可以先去干其他的活,不用再等了,干一会其他的活再问问来了没,这样循环。当操作系统准备好了之后,把数据由系统的缓冲区copy到应用程序。waite阶段是你可以利用的可以干其他的活。

 所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8083))
server.listen(5)
server.setblocking(False) #1True是阻塞,False是非阻塞
print('starting...')

rlist=[] #有连接来就不停的加
wlist=[] #send在数据量大的情况下也会阻塞
while True:
    try:
        conn, addr = server.accept() #2问操作系统有没有数据  ;服务端可以不停的建链接
        rlist.append(conn)  #有链接就建链接,每建立一个链接就放在列表里
        print(rlist)
    except BlockingIOError: #3捕捉这个异常就是那个操作系统发的信号,拿到这个异常然后干其他的活;利用accept那个waite阶段
        #print('干其他的活') #没有数据来就可以干其他的活了,

        #收消息
        del_rlist = [] #
        for conn in rlist: #取出conn去收消息
            try:
                data=conn.recv(1024)
                if not data: #收空消息,就跳过
                    del_rlist.append(conn)
                    continue 
                wlist.append((conn,data.upper())) #存放套接字以及准备发的数据
            except BlockingIOError: #捕捉异常,第一个套接字没有,可能下一个就来了
                continue #跳过
            except Exception: #可能套接字出现异常,客户端单方面的把连接断开;
                conn.close() #把么有用的给回收掉,del_rlist从删除它,不能在循环过程中删除
                del_rlist.append(conn)#

        #发消息
        del_wlist=[]
        for item in wlist:
            try:
                conn=item[0]#拿到conn对象
                data=item[1]#拿到数据
                conn.send(data) #缓冲区大小不能无线的大,send在数据量过大的情况下也会阻塞
                del_wlist.append(item)
            except BlockingIOError: #出异常了是内存满了,没抛异常证明发成功了
                pass

        for item in del_wlist:#发成功了把你删了 
            wlist.remove(item)

        for conn in del_rlist: #把没有数据发来的conn给删除掉
            rlist.remove(conn)


server.close()
View Code
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8083))

while True:
    msg=input('>>: ').strip()
    if not msg:continue
    client.send(msg.encode('utf-8'))
    data=client.recv(1024)
    print(data.decode('utf-8'))

client.close()
View Code

但是非阻塞IO模型绝不被推荐。

我们不能否则其优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在“同时”执行)。

但是也难掩其缺点: 

1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况
2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。
这会导致整体数据吞吐量的降低。

此外,在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成“作用的接口,例如select()多路复用模式,可以一次检测多个连接是否活跃。  

多路复用I/O模型

IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO。

直接发select(它是多路复用模型的一种,相当于一个中介,套接字有没有准备好由中介去问了,问操作系统数据有没有准备好,没有准备好它就在那等着;等到套接字数据准备好之后由服务端去发起个系统调用,是没有waite阶段的。)

多路复用经历了3个阶段,多了一个中间交互的阶段,性能还不如阻塞I/O,针对一个套接字;select高性能在于它可以监测多个套接字,而阻塞I/O只能有一个。

服务端的套接字有几类?conn,addr=server.accept()建链接;还有一个是建好链接的那个conn,负责recv和send操作。

from socket import *
import select

server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8083))
server.listen(5)
server.setblocking(False)
print('starting...')

rlist=[server,] #专门存收消息的 server调accept用来收;还有一个种是建完链接之后才能拿到的conn套接字;初始状态就是放server,后来可以放conn
wlist=[] #存发的数据
wdata={}

while True:
    rl,wl,xl=select.select(rlist,wlist,[],0.5) #去问操作系统套接字准备好没有;[]表示出异常的列表;0.5表示每隔0.5s去问一次;
    print('rl',rl)  #rlist会不断存conn和server一堆;wlist存一堆conn,缓冲区一旦没满证明就可以发了
    print('wl',wl)  #wl是可以往缓冲区send值的这种套接字

    for sock in rl: #读列表里边是干accept和recv的活
        if sock == server: #干链接的活
            conn,addr=sock.accept() #拿到conn的活
            rlist.append(conn)
        else:
            try:
                data=sock.recv(1024) #拿到数据,加到那个列表里边
                if not data:  #针对linux系统
                    sock.close()
                    rlist.remove(sock) #这个套接字不要监测了
                    continue
                wlist.append(sock) #把套接字加进去,还有它配套的数据
                wdata[sock]=data.upper()
            except Exception:#有可能客户端单方面把链接断开
                sock.close()
                rlist.remove(sock) ##rlist没有必要监测这个套接字了

    for sock in wl: #只能执行send操作
        data=wdata[sock]
        sock.send(data) #传数据
        wlist.remove(sock)#传完之后就把它给删了
        wdata.pop(sock)

server.close()
View Code
from socket import *

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8083))

while True:
    msg=input('>>: ').strip()
    if not msg:continue
    client.send(msg.encode('utf-8'))
    data=client.recv(1024)
    print(data.decode('utf-8'))

client.close()
View Code

该模型的优点:

相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。
如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

该模型的缺点:

首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。
很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异,
所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。
其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

异步I/O模型

 首先还是发一个系统调用,发送给操作系统内核之后,立马返回,返回之后就可以干其他活了。经历了waite和copy阶段,最后把数据直接丢给它。向操作系统说我要一个数据,数据请求发出去之后,操作系统你给我准备好数据,准备好之后给我送过来,这就是一种异步的方式。比阻塞I/O高,发起请求就直接返回了,没有经历waite和copy阶段

那是操作系统干的活;比起非阻塞I/O也高了,不用循环的去问了,它是什么时候好什么时候给我送回来。

猜你喜欢

转载自www.cnblogs.com/bj-mr-li/p/10007555.html