zoukankan      html  css  js  c++  java
  • 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.另种选择取一个小纸条上面有我的号码,等到排到我这一号时由柜台的人通知我轮到我去办理业务了;

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

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

  • 相关阅读:
    程序命名规则
    CSS样式常用命名参考
    转:数据挖掘资料收集
    javascript占位符
    网站目录,文件夹命名规范
    IIS HTTP 500 内部服务器错误完美解决 IIS 服务器无法加载应用程序 '/LM/W3SVC/1/ROOT'。错误是 '没有注册类别
    人事工资合同管理系统菜单截图
    Vs 正则表达式 查找替换 微软权威参考
    什么是DNS,A记录,子域名,CNAME别名,MX记录,TXT记录,SRV 记录,TTL值
    MT主机控制面板Plesk 使用指南
  • 原文地址:https://www.cnblogs.com/ldq1996/p/8781767.html
Copyright © 2011-2022 走看看