IO模型(多路复用IO)

IO模型

I/O(input/output):输入输出

IO所存在的问题:当我们要输入数据或是要输出数据时,通常需要很长一段时间(对于CPU而言)
而在等待的过程中,CPU就处于闲置状态,造成了资源的浪费

注意:IO其实有很多类型,例如:socket网络IO,内存到内存的copy,等待键盘输入。而这些对比起来socket网络IO需要等待的时间是最长的,也是我们重点关注的地方

而IO模型,就是要让我们在等待IO操作的过程中利用CPU,做别的事情

网络IO经历的步骤和过程

操作系统的两种状态:内核态和用户态。
当操作系统需要控制硬件时,例如接收网卡上的数据,必须先转到内核态,接受完数据后,要把数据从操作系统缓存区,copy到应用程序的缓冲区,也就是从内核态转为用户态。

接收数据的过程(如accept,recv):

  1. 等待数据到达网卡,叫做wait data
  2. 从内核copy到应用程序缓冲区,叫做copy data

发送数据的过程(如send):

  1. 数据从应用程序缓冲区copy到内核

阻塞IO模型

默认情况下,写出的TCP程序就是阻塞IO模型

该模型,提高效率方式:当你执行recv/accept,会进行wait data阶段。

  1. 你的进程会主动调用一个block指令,进程进入阻塞状态,同时让出CPU的执行权,操作系统就会将CPU分配给其他任务,从而提高了CPU的利用率
  2. 当数据到达时,首先会从内核将数据copy到应用程序缓冲区,并且socket将唤醒处于自身等待队列中的所有进程

之前使用多进程,多线程完成的并发,其实都是阻塞IO模型,每个线程在执行recv时,也会卡住

非阻塞IO模型

与阻塞IO模型相反,在调用recv,accept时都不会阻塞当前线程,

使用方法:将原本阻塞的socket设置为非阻塞

from socket import *
import time

server = socket()
server.bind(('127.0.0.1', 8000))
server.setblocking(False)	# 将阻塞变成非阻塞
server.listen(5)

cs = [] # 用来存放已连接的客户端
msgs = []   # 用来存放客户端发来的数据

while True:
    time.sleep(0.1) # 时间长了,客户端那边会有延迟,时间短了,对CPU的占用又会很大
    try:
        conn, client_addr = server.accept()
        print(client_addr)
        cs.append(conn)
    except BlockingIOError:
        print('没有人')
        # 代码执行到这里说明没有连接需要处理
        # 那就处理收发数据的任务
        for conn in cs[:]:  # 用来接收数据
            try:
                data = conn.recv(1024)
                if not data: raise ConnectionResetError

                print(data.decode('utf-8'))
                msgs.append((conn, data.upper()))

            except BlockingIOError:
                print('没有接受数据')
            except ConnectionResetError:
                conn.close()
                cs.remove(conn)

        for msg in msgs[:]: # 用来发送数据
            try:
                msg[0].send(msg[1])
                msgs.remove(msg)
            except BlockingIOError:
                pass
            except ConnectionResetError:
                msg[0].close()
                msgs.remove(msg)
                cs.remove(msg[0])

该模型在没有数据到达时,会报异常,而我们需要捕获异常,然后继续不断地询问系统内核,直到数据到达为止(有点像轮询)
可以看出,该模型会大量的占用CPU的资源去做一些无效的循环,效率低于阻塞IO模型。

报错的类型

  1. 服务端接受不到数据(accept,recv)的时候,会报错BlockingIOError
  2. 客户端正常退出,服务端会一直接受空
  3. 客户端断开了,再执行recv会报错ConnectionResetError
  4. 服务器不停地发送数据,就会把缓冲区塞满,然后报错BlockingIOError
  5. 客户端断开了,再执行send会报错ConnectionResetError

多路复用IO模型(重要)

属于事件驱动

多个socket使用同一套处理逻辑

以点餐为例:
非阻塞IO,相当于你每次去问,照着菜单挨个问好了没
多路复用则是你直接和前台说好你有哪些菜,然后直接问前台哪些菜做好了,前台则会给你返回一个列表,里面是已经做好的菜。

from socket import *
import select

server = socket()
server.bind(('127.0.0.1', 8000))
server.listen(5)
print('wait...')
r_list = [server]
w_list = []
msgs = {}

while True:
    # 如果所有盘子里都没有东西,就会阻塞在这里
    read_able, write_able, _ = select.select(r_list, w_list, [])

    # 处理可读的盘子,也就是接收数据
    for obj in read_able:   # 遍历所有可读数据的socket
        # 有可能是服务器,有可能是客户端
        if obj == server:
            conn, client_addr = server.accept()
            print(client_addr,'连接成功')
            r_list.append(conn)

        else:
            try:
                data = obj.recv(1024)
                if not data: raise ConnectionResetError
                print('收到一条新消息', data.decode('utf-8'))
                # 将要发送数据的socket加入到可写的盘子里让select检测
                w_list.append(obj)

                # 把要发送的数据以{对象:[数据,数据]}的格式放入容器
                if msgs.get(obj):
                    msgs[obj].append(data)
                else:
                    msgs[obj] = [data,]
            except ConnectionResetError:
                print('连接已断开')
                obj.close()
                r_list.remove(obj)
                msgs.pop(obj) #todo

    # 处理可写的盘子,也就是send数据
    for obj in write_able:
        msg_list = msgs.get(obj)
        if msg_list:
            # 遍历发送所有的数据
            for i in msg_list:
                try:
                    obj.send(i)
                except ConnectionResetError:
                    msgs.pop(obj)
                    w_list.remove(obj)
                    break
            # 把数据从容器中删除
            msgs.pop(obj)
        # 把这个socket从w_list中删除
        w_list.remove(obj)

对比之前提到的阻塞或是非阻塞IO模型,增加了一个select,来帮我们检测socket的状态,从而避免了我们自己去检测socket带来的开销

select有四个盘子,是否可读,是否可写,可读,可写,异常的先不管,自己处理异常
我们只要将任务放入是否可读可写的盘子中,那么一旦任务就绪了,就会返回到可读可写的盘子中,而我们则需要遍历列表,再分别处理读写即可。

多路复用对比非阻塞

优势:多路复用可以极大降低CPU的占用率。

弊端:多路复用本质,多个任务之间是串行的,如果某个任务的耗时较长,将导致其他的任务无法立即执行。

所以多路复用的最大优点就是高并发,但是能够处理的数据不会太大(如果需要处理的数据量大,也可以一部分一部分的处理)。

异步IO模型

异步IO != 非阻塞IO

因为非阻塞IO在copy data的过程是一个同步的过程,会卡住主线程,只是比较快

而异步IO,是发起任务后,就可以继续执行其他任务,只有当数据已经拷贝到应用程序缓存区,才会给你的线程发送信号,或者执行回调

asyncio

信号驱动IO模型

简单地说,就是当某个事情发送后,会给你的线程发送一个信号,那么你的线程就可以去处理这个任务了

不常用的原因是,socket的信号太多了,处理起来非常繁琐

posted @ 2019-07-09 23:28  abcde_12345  阅读(339)  评论(0编辑  收藏  举报