TCP 编程(五)

116 阅读3分钟

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战」。

这次我们介绍一下所谓的 IO 复用。首先说明下这个东西出现的原因。尽管我们先前考量了一些处理 socket 连接的方式,使用线程或者进程这些。但是有些因素我们没有考虑,这些东西是有开销的,而且开销并不低。

这样说可能还是抽象了一些,我们换个角度,假设我们只有一个 CPU ,我们希望利用这些并发技术的愿意是为了让 CPU 尽量的做更多的事。假设程序同时只处理一个 socket 连接,这里的会有阻塞操作,而这个阻塞可以认为是不消耗 CPU 资源的,阻塞在此处可以认为 CPU 在空转(IDLE进程),这时可以让它做一些其他的事情,比如读另一个 socket (实际也不局限于socket ,因为这里考虑的是面向 IO )。

其实这两个程序做的是同样的事情,那么可以发现,第二个也会陷入第一个程序的处境,也就是等,而后可以可以读取了再进行读取操作。两个程序的中间某一段时间都有种浪费资源的意味。当然,例子有点极端,但大致的考量是这样。

为了避免这种情况,人们就想了一个办法:用一个程序来处理这些操作,比如说这里的读取操作,那个能运行我就运行哪个,不能运行我就统一的阻塞,等有可运行的程序。

如果把运行的操作看作原来的程序(进程或者线程),这里有点调度的意味了。每次把可以运行的程序选出赖运行。

为了更直观一点,我们使用一个代码的例子,假设我们只有一段程序,读取输入并输出。什么叫它可以运行呢?也就是标准输入可以读取了(有数据可以读取)。

为了使用所谓的复用,这里将使用 select 方法,代码如下

try:
    while True:
        reable_decripter, _, _ = select.select([sys.stdin.fileno()], [], [], None)
        if sys.stdin.fileno() in reable_decripter: # 相当于是可运行
        # 注释之间为我们的程序
            print("hello")
            print(input())
        # 
except KeyboardInterrupt:
    pass

先不管 select 行的含义,可以发现的是,在运行时并没有输出,需要我们输入一行内容并按回车(因为标准输是行缓冲的)才会输出。接着会输出我们的输入内容,然后再循环。

现在再看 select 这一行,它的参数是三个列表(可读,可写,异常),和一个超时参数。返回值则可以认为是所谓的可以运行的。值得注意是这里列表使用的是文件描述符。

select是一个阻塞操作,当有 '可以运行' 的程序之后,它会返回值,然后进入到下方,这时我们可以通过检测文件描述符的状态决定程序是否运行。就好比是我们上面的 in 操作。

尽管我们这里只有一个 '程序',但是可以很容易的可以迁移到更多 '程序' 。同样的,套接字本身就是一个文件描述符也是一样的操作。

再考虑下所谓的条件检测,可以发现它是按顺序写的,如果有多个程序同时就绪,可以认为这里是一个手动调度的过程,一如我们前文所述。

到这里所谓的 IO 多路复用已经介绍完了。可以发现的是,其实和他的名字并不是很相符,如果要更准确的描述它,可能使用线程或者说进程复用更合适。