IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间 --> 内核空间、内核空间-->设备空间(磁盘、网络等)。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。

通俗理解:让CPU等待的就是IO,而输入/输出就是一个等待的过程,也可以将IO理解为就是阻塞

IO模型就是为了说明协程遇到IO操作切换执行另一个函数,当IO执行完后是如何切换回来执行原来的函数的

5种IO模型:

  • 阻塞IO ->blocking IO -> 工作效率低

  • 非阻塞IO -> nonblocking IO -> 工作效率高,CPU负担大 -> 不建议使用

  • IO多路复用-> IO multiplexing -> 在有多个对象需要IO阻塞的时候,能够有效的减少阻塞带来的时间损耗,且能够在一定程度上减少CPU的负担

  • 异步IO-> asynchronous IO -> 工作效率高,CPU负担少,无法直接通过代码实现,只能借助操作系统一些机制/接口或Python的内置模块实现,asyncio是Python的内置异步IO模块,通过它就能实现异步IO

  • 信号驱动IO-> signal driven IO -> 并不常用

1. 阻塞IO


# 平时我们写代码遇到的就是IO阻塞,如 recv

2. 阻塞IO


  • 通过代码实现非阻塞IO

# server.py

import socket

sk = socket.socket()
sk.bind(('127.0.0.1', 8080))
sk.listen()
sk.setblocking(False)  # 将socket设置为非阻塞模式

conn_lis = []
while True:
    try:
        conn, addr = sk.accept()
        conn_lis.append(conn)
    except BlockingIOError:
        del_lis = []
        for c in conn_lis:
            try:
                msg = c.recv(10).decode('utf-8')
                if not msg:
                    c.close()
                    del_lis.append(c)
                else:
                    print(msg)
                    c.send(msg.upper().encode('utf-8'))
            except BlockingIOError:
                pass
        if del_lis:
            for del_item in del_lis:
                conn_lis.remove(del_item)

# client.py

import socket
import time
from threading import Thread


def fun():
    sk = socket.socket()
    sk.connect(('127.0.0.1', 8080))
    time.sleep(1)
    sk.send(b'hello')
    print(sk.recv(10).decode('utf-8'))
    sk.close()


for i in range(10):
    Thread(target=fun).start()

3. IO多路复用


  • 如果只有一个IO阻塞的时候就不建议使用IO多路复用,直接使用IO阻塞就可以了,因为IO多路复用只有在有多个对象需要IO阻塞的时候使用,只有一个IO阻塞的时候使用IO多路复用反而会降低了执行效率(因为IO多路复用执行的过程中中间还多了一个通知用户态的过程)

  • 使用select模块实现IO多路复用

# server.py

import socket
import select

sk = socket.socket()
sk.bind(('127.0.0.1', 8080))
sk.listen()

read_lst = [sk]  # 输入列表
while True:
    rl, wl, xl = select.select(read_lst, [], [])  # 监听输入列表中sk或多个conn是否有用户进行连接(是否有数据返回),如果有就会返回一个列表中包含 以被用户连接的sk 或 多个得到数据的conn对象 (因为有多个客户端对服务端进行连接,所以才会有多个conn对象)
    for item in rl:  # 循环select的返回值中的输入列表,因为里面包含了 以被用户连接的sk 或 多个得到数据的conn对象(因为有多个客户端对服务端进行连接,所以才会有多个conn对象)
        if item == sk:  # 判断是否是sk对象(因为sk对象只会有一个,并且是同一个),如果等于sk对象那么就代表有新的客户端对服务端进行连接
            conn, addr = item.accept()  # 得到一个conn客户端对象
            read_lst.append(conn)  # 将 与服务端进行链接了的客户端对象放入 read_lst 输入列表中
        else:  # 如果不是sk对象就是conn对象
            ret = item.recv(1024).decode('utf-8')
            if not ret:  # 判断接收到的消息是否为空,如果是就说明客户端已经断开连接这样就关闭该客户端并且从read_lst输入列表中删除,如果不为空就打印该数据并且向客户端回一条消息
                item.close()
                read_lst.remove(item)
            else:
                print(ret)
                item.send(ret.upper().encode('utf-8'))

# client.py

import socket
import time
from threading import Thread


def fun():
    sk = socket.socket()
    sk.connect(('127.0.0.1', 8080))
    for i in range(10):
        time.sleep(2)
        sk.send(b'hello')
        print(sk.recv(1024))
    sk.close()


for i in range(10):
    Thread(target=fun).start()