zoukankan      html  css  js  c++  java
  • IO模型浅析

    同步、异步、阻塞、非阻塞

    同步:
    所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。

    例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

    异步:
    异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

    例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

    阻塞
    阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。

    有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。

    非阻塞
    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

    对象的阻塞模式和阻塞函数调用
    对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。

    1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
    2. 异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)
    3. 阻塞, 就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
    4. 非阻塞, 就是调用我(函数),我(函数)立即返回,通过select通知调用者
      同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞!
      阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!

    同步与异步:描述的是用户线程与内核的交互方式,同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍然继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

    阻塞和非阻塞:描述的是用户线程调用内核IO操作的方式,阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

    同步阻塞IO

    应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。默认情况下所有的Socket是阻塞。

    用户线程通过系统调用read发起IO操作,由用户空间转到内核空间,内核等待数据到达后,然后将数据从内核拷贝到用户线程。即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。

    在python网络编程中,socket的accept()和recv()都是阻塞的。

    同步非阻塞IO

    同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK,这样做用户线程可以在发起IO请求后可以立即返回。设置为非阻塞后,用户线程发起IO请求时立即返回。如果没读取到数据,用户线程需要不断地发起IO请求,直到数据到达后才真正读取到数据继续执行。

    即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。而且在数据拷贝阶段没做任何改变,这个阶段依然是阻塞的。

    IO多路复用

    IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

    户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

    从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

    虽然此方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。

    select、poll、epoll简析

    select特点

    1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。最大链接数限额(1024)

    2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

    3、需要维护一个用来存放大量fd的数据结构,遍历所有的文件描述符(fd)查看是否有数据访问,这样会使得用户空间和内核空间在传递该结构时复制开销大

    poll
    poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

    epoll
    1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);

    2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;

    即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

    3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

    python示例代码(select)

    #server端
    import socket
    import select
    sock = socket.socket()
    sock.bind(("127.0.0.1",9000))
    sock.listen(5)
    li = [sock]
    while 1:
        r = select.select(li,[],[])     #三个列表参数,输入列表(要监听的列表),输出列表,错误列表
        for i in r[0]:        #r[0]就是监听到变化的列表(fd变化)
            if i == sock:      #i == sock时说明新的客户端连接
                conn,addr = i.accept()
                li.append(conn)     #将conn对象追加到监听列表
            else:      #i != sock时说明是客户端有数据发送
                data = i.recv(1024)
                print(data.decode('utf-8'))
                response = input(">>>").strip()   #input阻塞
                i.send(response.encode('utf-8'))
    
    #client端
    import socket
    sock = socket.socket()
    sock.connect(("127.0.0.1",9000))
    while 1:
        data = input('>>>').strip()
        sock.send(data.encode('utf-8'))
        response = sock.recv(1024)
        print(response.decode('utf-8'))
    

    高效IO多路复用

    #高效IO多路复用(selectors)
    import selectors
    import socket
    sel = selectors.DefaultSelector()
    def accept(sock,mask):
        conn,addr = sock.accept()
        sel.register(conn,selectors.EVENT_READ,read)
    def read(conn,mask):
        data = conn.recv(1024)
        print(data.decode('utf-8'))
        response = input(">>>").strip()
        conn.send(response.encode('utf-8'))
    sock = socket.socket()
    sock.bind(("127.0.0.1",9000))
    sock.listen(5)
    sock.setblocking(False)
    sel.register(sock,selectors.EVENT_READ,accept)  #注册,绑定套接字对象和函数
    while True:
        events = sel.select()    #监听套接字对象(所有注册的)
        for key,mask in events:
            callback = key.data      #key.data就是套接字对象绑定的那个函数
            callback(key.fileobj,mask)   #执行函数
    
    #client端
    import socket
    sock = socket.socket()
    sock.connect(("127.0.0.1",9000))
    while 1:
        data = input('>>>').strip()
        sock.send(data.encode('utf-8'))
        response = sock.recv(1024)
        print(response.decode('utf-8'))
    

    信号驱动IO

    简介:两次调用,两次返回。

    首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

    异步IO

    数据拷贝的时候进程无需阻塞。异步IO不会引起进程阻塞。

    当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作

    5种IO模型比较

  • 相关阅读:
    vue脚手架搭建项目
    springmvc上传下载文件
    vue双向绑定(模型变化,视图变化,反之亦然)
    android中广告轮播图总结
    studio插件
    系统图片uri的问题
    android
    mysql 外键(FOREIGN KEY)使用介绍
    不用加减乘除来做加法的题目
    Comparable接口实现和使用方法介绍
  • 原文地址:https://www.cnblogs.com/liao-lin/p/7219956.html
Copyright © 2011-2022 走看看