IO模型

IO模型

标签:并发编程

  • 阻塞IO
  • 非阻塞IO
  • IO多路复用
  • 信号驱动
  • 异步IO

阻塞IO

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

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

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

只能同时一个客户端进行连接,只有一个客户端关闭另一个才会连接。

服务端

from socket import *

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

while True:
    '''链接循环'''
    conn, addr = server.accept()  #等待连接阻塞状态
    print(addr)
    while True:
        '''通信循环'''
        try:
            data = conn.recv(1024)  #接收数据阻塞状态
            if not data: break
            conn.send(data.upper())
        except Exception:
            break
    conn.close()
server.close()

客户端

from socket import *

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

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

非阻塞IO

从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情,或者直接再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存(这一阶段仍然是阻塞的),然后返回。

也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

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

服务端

from socket import *

server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8001))
server.listen(5)
server.setblocking(False)
print('starting...')
conn_l = []
del_l = []

while True:
    '''链接循环'''
    try:
        # print(conn_l)
        conn, addr = server.accept() #
        conn_l.append(conn)
    except BlockingIOError:
        '''收不到数据的时候执行这步操作 wait data'''
        for conn in conn_l:
            try:
                data = conn.recv(1024)
                conn.send(data.upper())
            except BlockingIOError:
                continue
            except ConnectionError:
                del_l.append(conn)   #for循环不能删除迭代某个数据
        for obj in del_l:
            conn_l.remove(obj)
        del_l = []


'''缺点:CPU负载严重,2.响应时间长,每过一段时间轮询一次read操作,导致整体任务吞吐量降低'''

客户端

from socket import *

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

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

客户端完全不变

IO多路复用

当用户进程调用了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的优势在于可以处理多个连接,不适用于单个连接

select

服务端

from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1', 8002))
server.listen(5)
server.setblocking(False)
print('starting...')
reads_l = [server,]
while True:
    r_l,_,_ = select.select(reads_l,[],[])     #监测哪个socket
    print(r_l)
    for obj in r_l:
        if obj == server:
            conn,addr = obj.accept()
            reads_l.append(conn)
        else:
            data = obj.recv(1024) #obj = conn
            obj.send(data.upper())

客户端

from socket import *

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

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

客户端与前部分相同

poll

服务端

客户端

同步、异步、阻塞、非阻塞的区别

同步与异步

同步异步 指的是在客户端

同步意味着 客户端提出了一个请求以后,在回应之前只能等待

异步意味着 客户端提出一个请求以后,还可以继续提其他请求

阻塞非阻塞 指的是服务器端

阻塞意味着 服务器接受一个请求后,在返回结果以前不能接受其他请求

非阻塞意味着 服务器接受一个请求后,尽管没有返回结果,还是可以继续接受其他请求

首先来解释同步和异步的概念,这两个概念与消息的通知机制有关。

概念描述

所谓同步就是一个任务完成需要依赖另外一个任务时,只有等待另一个任务任务完成后,这个任务才可以完成。要么都成功要么都失败,两个任务状态可以保持一致。

所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

消息通知

异步的概念和同步相对。当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

这里提到执行部件和调用者通过三种途径返回结果:状态、通知和回调。使用哪一种通知机制,依赖于执行部件的实现,除非执行部件提供多种选择,否则不受调用者控制。

1.如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低(有些初学多线程编程的人,总喜欢用一个循环去检查某个变量的值,这其实是一种很严重的错误);

2.如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。至于回调函数,其实和通知没太多区别。

场景比喻

举个例子,比如我去银行办理业务,可能会有两种方式:

1.选择排队等候;

2.另种选择取一个小纸条上面有我的号码,等到排到我这一号时由柜台的人通知我轮到我去办理业务了;

第一种:前者(排队等候)就是同步等待消息通知,也就是我要一直在等待银行办理业务情况;

第二种:后者(等待别人通知)就是异步等待消息通知。在异步消息处理中,等待消息通知者(在这个例子中就是等待办理业务的人)往往注册一个回调机制,在所等待的事件被触发时由触发机制(在这里是柜台的人)通过某种机制(在这里是写在小纸条上的号码,喊号)找到等待该事件的人。

原文地址:https://www.cnblogs.com/ldq1996/p/8781767.html