python基础-io模型、阻塞、非阻塞、io多路复用

io介绍

为了更好地了解IO模型,我们需要事先回顾下:同步、异步、阻塞、非阻塞

  1. 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。

  2. 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程

IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:

1)等待数据准备 (Waiting for the data to be ready)
2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

阻塞IO(blocking IO)

这里写图片描述

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。

而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

ps:所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

我们来看下ftp的一个阻塞io的例子
服务端:

from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8081))
server.listen(5)

while True:
    conn, addr = server.accept()
    print(addr)

    while True:
        try:
            data=conn.recv(1024)
            if not data:break
            conn.send(data.upper())
        except ConnectionResetError:
            print("ConnectionResetError")
            break
    conn.close()

server.close()

客户端:

from socket import *

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

while True:
    msg=input('>>: ').strip()
    if not msg:continue
    client.send(msg.encode('utf-8'))
    data=client.recv(1024)
    print(data.decode('utf-8'))
一个简单的解决方案:

在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

该方案的问题是:

开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。

改进方案:    

很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。

改进后方案其实也存在着问题:

“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

非阻塞IO(non-blocking IO)

这里写图片描述

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。
也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态

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

服务端:

#1 对cpu的占用率过多,但是是无用的占用
#2 在连接数过多的情况下,不能及时响应客户的的消息

from socket import *
import time
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8083))
server.listen(5)
server.setblocking(False)

conn_l=[]

while True:
    try:
        conn, addr = server.accept()
        conn_l.append(conn)
        print(addr)
    except BlockingIOError:
        print('干其他活去了',len(conn_l))
        del_l = []
        for conn in conn_l:
            try:
                data=conn.recv(1024)
                if not data:
                    conn.close()
                    del_l.append(conn)
                    continue
                conn.send(data.upper())
            except BlockingIOError:
                pass
            except ConnectionResetError:
                conn.close()
                del_l.append(conn)

        for conn in del_l:
            conn_l.remove(conn)

客户端:

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'))

多路复用IO(IO multiplexing)

它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
这里写图片描述

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

  1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

    1. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

    结论: select的优势在于可以处理多个连接,不适用于单个连接

我们看一个例子
客户端:

from socket import *

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

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

服务端:


from socket import *
import time
import select

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

read_l=[server]
print('starting....')
while True:
    #rlist --等到准备阅读
    #wlist --等到准备写作
    #xList --等待` `例外条件”
    rl,wl,xl=select.select(read_l,[],[]) #read_l=[server,conn1,conn2,conn3]
    #rl=[server,conn2]

    # print('===>',rl)
    # print(rl[0] is server)

    for r in rl:
        if r is server:
            conn,addr=rl[0].accept() #conn,addr=server.accpet()
            print(addr)
            read_l.append(conn)
            # print(len(read_l))
        else:
            try:
                data=r.recv(1024)
                if not data:
                    r.close()
                    read_l.remove(r)
                    continue
                r.send(data.upper())
            except ConnectionResetError:
                r.close()
                read_l.remove(r)

上面代码的意思是:select.select
select函数阻塞程序运行,监控read_l中的套接字,当其中有套接字满足可读的条件(第一个参数为可读,如果是第二个参数则为可写),则把这个套接字返回给rl,然后程序继续运行。

至于套接字怎么才算可读呢?搜索可知,当套接字缓冲区大于1byte时,就被标记为可读。也就是说,当套接字收到客户端发来的数据,就变成可读,然后select就会把这个套接字取出来,进入下一步程序。

我们通过调试来看下输出日志信息
1、启动服务端
2、启动客户端1
3、启动客户端2
4、在客户端1输入信息
5、在客户端2输入信息

以下是服务端的输出(我简化了):

#启动服务端、客户端1
starting....
read_l [<socket.socket laddr=('127.0.0.1', 8084)>]

True ,rl: [<socket.socket laddr=('127.0.0.1', 8084)>]

<socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56868)>

#启动客户端2
read_l [<socket.socket laddr=('127.0.0.1', 8084)>, <socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56868)>]

True ,rl: [<socket.socket laddr=('127.0.0.1', 8084)>]

<socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56870)>


#客户端1发送消息
read_l [<socket.socket laddr=('127.0.0.1', 8084)>, <socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56868)>, <socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56870)>]

False ,rl: [<socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56868)>]


#客户端2发送消息
read_l [<socket.socket laddr=('127.0.0.1', 8084)>, <socket.socket
laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56868)>, <socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56870)>]

False ,rl: [<socket.socket laddr=('127.0.0.1', 8084), raddr=('127.0.0.1', 56870)>]

所以列表初始状态要监测server套接字,监测的是具有阻塞行为的,通过上述的输出就能了解其流程了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值