电子说
大家好,我是小风哥,今天我们来聊聊协程的作用。
假设磁盘上有10个文件,你需要读取的内存,那么你该怎么用代码实现呢?
在接着往下看之前,先自己想一想这个问题,看看自己能想出几种方法,各自有什么样的优缺点。想清楚了吗(还在看吗),想清楚了我们继续往下看。
for file in files: result = file.read() process(result)
是不是非常简单,我们假设每个文件读取需要1分钟,那么10个文件总共需要10分钟才能读取完成。这种方法有什么问题呢?实际上,这种方法只有一个问题,那就是慢!除此之外,其它都是优点,比如:代码简单,容易理解
可维护性好,这代码交给谁都能维护的了(论程序员的核心竞争力在哪里)
def read_and_process(file):
result = file.read()
process(result)
def main():
files = [fileA,fileB,fileC......]
for file in files:
create_thread(read_and_process,
file).run()
# 等待这些线程执行完成
怎么样,是不是也非常简单。那么这种方法有什么问题吗?在开启10个线程这种问题规模下没有问题。现在我们把问题难度加大,假设有10000个文件,需要处理该怎么办呢?有的同学可能想10个文件和10000个文件有什么区别吗,直接创建10000个线程去读不可以吗?实际上,这里的问题其实是说创建多个线程有没有什么问题。我们知道,虽然线程号称“轻量级进程”,虽然是轻量级但当数量足够可观时依然会有性能问题。这里的问题主要有这样几个方面:创建线程需要消耗系统资源,像内存等(想一想为什么?)
调度开销,尤其是当线程数量较多且都比较繁忙时(同样想一想为什么?)
创建多个线程不一定能加快I/O(如果此时设备处理能力已经饱和)
相对于CPU的处理速度来说,IO是非常慢的
IO不怎么需要计算资源
event_loop = EventLoop()
然后,我们需要往event loop中加入原材料,也就是需要监控的event,就像这样:def add_to_event_loop(event_loop, file):
file.asyn_read() # 文件异步读取
event_loop.add(file)
注意,当执行file.asyn_read这行代码时会立即返回,不会阻塞线程,当这行代码返回时可能文件还没有真正开始读取,这就是所谓的异步。file.asyn_read这行代码的真正目的仅仅是发起IO,而不是等待IO执行完成。此后,我们将该IO放到event loop中进行监控,也就是event_loop.add(file)这行代码的作用。一切准备就绪,接下来就可以等待event的到来了:while event_loop:
file = event_loop.wait_one_IO_ready()
process(file.result)
我们可以看到,event_loop会一直等待直到有文件读取完成(event_loop.wait_one_IO_ready())。这时,我们就能得到读完的文件了,接下来处理即可。全部代码如下所示:def add_to_event_loop(event_loop, file):
file.asyn_read() # 文件异步读取
event_loop.add(file)
def main():
files = [fileA,fileB,fileC ...]
event_loop = EventLoop()
for file in files:
add_to_event_loop(event_loop, file)
while event_loop:
file = event_loop.wait_one_IO_ready()
process(file.result)
def add_to_event_loop(event_loop, file):
file.asyn_read() # 文件异步读取
event_loop.add(file)
def main():
files = [fileA,fileB,fileC......]
event_loop = EventLoop()
for file in files:
add_to_event_loop(event_loop, file)
while event_loop:
file = event_loop.wait_one_IO_ready()
process(file.result)
对于add_to_event_loop,由于文件异步读取,因此该函数可以瞬间执行完成,真正耗时的函数其实就是event loop的等待函数,也就是这样:file = event_loop.wait_one_IO_ready()
我们知道,一个文件的读取耗时是1秒,因此该函数在1s后才能返回,但是,但是,接下来是重点。但是,虽然该函数wait_one_IO_ready会等待1s,不要忘了,我们利用这两行代码同时发起了10个IO操作请求。
for file in files: add_to_event_loop(event_loop, file)
因此,在event_loop.wait_one_IO_ready等待的1s期间,剩下的9个IO也完成了。也就是说,event_loop.wait_one_IO_ready函数只是在第一次循环时会等待1s,但此后的9次循环会直接返回,原因就在于剩下的9个IO也完成了。因此,整个程序的执行耗时也是1秒。是不是很神奇,我们只用一个线程就达到了10个线程的效果。这就是event loop + 异步的威力所在。
def IO_type_1(event_loop, io):
io.start()
def callback(result):
process_IO_type_1(result)
event_loop.add((io, callback))
这样,event_loop在检测到有IO完成后就可以把该IO和关联的callback处理函数一并检索出来,直接调用callback函数就可以了。while event_loop:
io, callback = event_loop.wait_one_IO_ready()
callback(io.result)
看到了吧,这样event_loop内部就极其简洁了,even_loop根本就不关心该怎么处理该IO结果,这是注册的callback该关心的事情,event_loop需要做的仅仅就是拿到event以及相应的处理函数callback,然后调用该callback函数就可以了。现在我们可以同单线程来并发编程了,也使用callback对IO处理进行了抽象,使得代码更加容易维护,想想看还有没有什么问题?
def start_IO_type_1(event_loop, io):
io.start()
def callback(result):
process_IO_type_1(result)
event_loop.add((io, callback))
从上述代码中你能看到什么问题吗?在上述代码中,一次IO处理过程被分为了两个部分:发起IO
IO处理
def start_IO_type_1(io):
io.start() # IO异步请求
yield # 暂停当前协程
process_IO_type_1(result) # 处理返回结果
此后,我们要把该协程放到event loop中监控起来:def add_to_event_loop(io, event_loop):
coroutine = start_IO_type_1(io)
next(coroutine)
event_loop.add(coroutine)
最后,当IO完成后event loop检索出相应的协程并恢复其运行:while event_loop:
coroutine = event_loop.wait_one_IO_ready()
next(coroutine)
现在你应该看出来了吧,上述代码中没有回调,也没有把处理IO的流程拆成两部分,整体的代码都是以同步的方式来编写,最棒的是依然能达到异步的效果。实际上你会看到,采用协程后我们依然需要基于事件编程的event loop,因为本质上协程并没有改变IO的异步处理本质,只要IO是异步处理的那么我们就必须依赖event loop来监控IO何时完成,只不过我们采用协程消除了对回调的依赖,整体编程方式上还是采用程序员最熟悉也最容易理解的同步方式。
单线程串行 + 阻塞式IO(同步)
多线程并行 + 阻塞式IO(并行)
单线程 + 非阻塞式IO(异步) + event loop
单线程 + 非阻塞式IO(异步) + event loop + 回调
Reactor模式(更好的单线程 + 非阻塞式IO+ event loop + 回调)
单线程 + 非阻塞式IO(异步) + event loop + 协程
审核编辑 :李倩
全部0条评论
快来发表一下你的评论吧 !