在接着往下看之前,先自己想一想这个问题,看看自己能想出几种方法,各自有什么样的优缺点。
这可能是大多数同学都能想到的最简单方法,那就是一个一个的读取,读完一个接着读下一个。
这种方法有什么问题呢?
除此之外,其它都是优点:
- 可维护性好,这代码交给谁都能维护的了(论程序员的核心竞争力在哪里)
有的同学可能已经想到了,为啥要一个一个读取呢?并行读取不就可以加快速度了吗。
稍好的方法,并行
显然,地球人都知道,线程就是用来并行的。
用代码实现就是这样的:
def read_and_process(file):
result = filerun()
# 等待这些线程执行完成
那么这种方法有什么问题吗?
现在我们把问题难度加大,假设有10000个文件,需要处理该怎么办呢?
实际上这里的问题其实是说创建多个线程有没有什么问题。
这里的问题主要有这样几个方面:
- 调度开销,尤其是当线程数量较多且都比较繁忙时(同样想一想为什么?)
- 既然线程有这样那样的问题,那么还有没有更好的方法?
这里的答案就是基于事件驱动编程技术。
事件驱动 + 异步
这是基于这样两个事实:
- IO不怎么需要计算资源
这就是为什么单线程也可以并行处理多个IO的本质所在。
实际上非常简单。
然后,我们需要往event loop中加入原材料,也就是需要监控的event,就像这样:
def add_to_event_loop(event_loop, file): fileadd(file)
我们可以看到,event_loop会一直等待直到有文件读取完成(event_loop5px;">全部代码如下所示:
def add_to_event_loop(event_loop, file): fileadd(file) def main(): files = [fileA,fileB,fileC wait_one_IO_ready() process(file5px;">接下来我们看下程序执行的效果。
那么对于单线程+event loop呢?
对于add_to_event_loop,由于文件异步读取,因此该函数可以瞬间执行完成,真正耗时的函数其实就是event loop的等待函数,也就是这样:
file = event_loop5px;">我们知道,一个文件的读取耗时是1秒,因此该函数在1s后才能返回,但是,但是,接下来是重点。
因此在event_loopwait_one_IO_ready函数只是在第一次循环时会等待1s,但是此后的9次循环会直接返回,原因就在于剩下的9个IO也完成了。
是不是很神奇,我们只用一个线程就达到了10个线程的效果。
本质上,我们上述给出的event loop简单代码片段做的事情本质上和生物一样:
我们这里的给出event,然后处理event。
现在你应该明白所谓的Reactors模式是怎么一回事了吧。
如果我们需要处理各种类型的IO上述代码片段会有什么问题吗?
幸好我们也有应对策略,这就是回调。关于回调函数,请参考这篇《程序员应如何理解回调函数》。
就像这样:
def IO_type_1(event_loop, io): ioadd((io, callback))
看到了吧,这样event_loop内部就极其简洁了,even_loop根本就不关心该怎么处理该IO结果,这是注册的callback该关心的事情,event_loop需要做的仅仅就是拿到event以及相应的处理函数callback,然后调用该callback函数就可以了。
虽然回调函数使得event loop内部更加简洁,但依然有其它问题,让我们来仔细看看回调函数:
def start_IO_type_1(event_loop, io): ioadd((io, callback))
在上述代码中,一次IO处理过程被分为了两部分:
- IO处理
这里的给的例子很简单,所以你可能不以为意,但是当处理的任务非常复杂时,可能会出现回调函数中嵌套回调函数,也就是回调地狱,这样的代码维护起来会让你怀疑为什么要称为一名苦逼的码农。
问题出在哪里
同步编程模式下很简单,但是同步模式下发起IO,线程会被阻塞,这样我们就不得不创建多个线程,但是创建过多线程又会有性能问题。
在这种模式下,异步发起IO不会阻塞调用线程,我们可以使用单线程加异步编程的方法来实现多线程效果,但是在这种模式下处理一个IO的流程又不得不被拆分成两部分,这样的代码违反程序员直觉,因此难以维护。
答案是肯定的,这就是协程。关于协程请参考《程序员应如何理解协程》。
Finally!终于到了协程
这是什么意思呢?
而协程最棒的一点就在于挂起后可以暂存执行状态,恢复运行后可以在挂起点继续运行,这样我们就不再需要像回调那样将一个IO的处理流程拆分成两部分了。
接下来我们就用协程来改造一下回调版本的IO处理方式:
def start_IO_type_1(io): io5px;">此后我们要把该协程放到event loop中监控起来:
def add_to_event_loop(io, event_loop): coroutine = start_IO_type_1(io) next(coroutine) event_loop5px;">最后,当IO完成后event loop检索出相应的协程并恢复其运行:
while event_loop: coroutine = event_loop5px;">现在你应该看出来了吧,上述代码中没有回调,也没有把处理IO的流程拆成两部分,整体的代码都是以同步的方式来编写,最棒的是依然能达到异步的效果。
看上去简简单单的IO实际上一点都不简单吧。
单线程串行 + 阻塞式IO(同步)
- 单线程 + 非阻塞式IO(异步) + event loop
- Reactor模式(更好的单线程 + 非阻塞式IO+ event loop + 回调)
- 最终我们采用协程技术获取到了异步编程的高效以及同步编程的简单理解,这也是当今高性能服务器常用的一种技术组合。