python异步框架中协程之间的并行 asyncio tornado.gen

python异步框架中协程之间的并行

前言

python中的异步协程框架有很多,比如 tornado , gevent , asyncio , twisted 等。协程带来的是低消耗的并发,在等待IO事件的时候可以把控制权交给其它的协程,这个是它并发能力的保障。但是光有并发还是不够的,高并发并不能保证低延迟,因为一个业务逻辑的流程可能包含多个异步IO的请求,如果这些异步IO的请求是一个一个逐步执行的,虽然server的吞吐量还是很高,但是每个请求的延迟就会很大。为了解决这类问题,每个框架都有各自不同的方式,下面我们就来分别看看,它们都是怎么管理互不相关协程之间的并行的。

asyncio

python3.4及以上

在我的博客里有一篇关于asyncio库的译文,里面最后一部分就有介绍它是如何管理互不相关的协程的。这里我们还是引用它,并给他增加了计时的功能来更好地阐述协程是如何并行的:

import asyncio
import random
import time


@asyncio.coroutine
def get_url(url):
wait_time = random.randint(1, 4)
yield from asyncio.sleep(wait_time)
print('URL {} took {}s to get!'.format(url, wait_time))
return url, wait_time


@asyncio.coroutine
def process_as_results_come_in():
before = time.time()
coroutines = [get_url(url) for url in ['URL1', 'URL2', 'URL3']]
for coroutine in asyncio.as_completed(coroutines):
url, wait_time = yield from coroutine
print('Coroutine for {} is done'.format(url))
after = time.time()
print('total time: {} seconds'.format(after - before))


@asyncio.coroutine
def process_once_everything_ready():
before = time.time()
coroutines = [get_url(url) for url in ['URL1', 'URL2', 'URL3']]
results = yield from asyncio.gather(*coroutines)
print(results)
after = time.time()
print('total time: {} seconds'.format(after - before))


def main():
loop = asyncio.get_event_loop()
print("First, process results as they come in:")
loop.run_until_complete(process_as_results_come_in())
print("\nNow, process results once they are all ready:")
loop.run_until_complete(process_once_everything_ready())


if __name__ == '__main__':
main()

代码输出:

$ python3 asyncio_test.py
First, process results as they come in:
URL URL3 took 1s to get!
Coroutine for URL3 is done
URL URL2 took 2s to get!
Coroutine for URL2 is done
URL URL1 took 3s to get!
Coroutine for URL1 is done
total time: 3.001127004623413 seconds

Now, process results once they are all ready:
URL URL1 took 1s to get!
URL URL3 took 1s to get!
URL URL2 took 4s to get!
[('URL1', 1), ('URL2', 4), ('URL3', 1)]
total time: 4.004215955734253 seconds

asyncio里面,给了两种方案,都是可以做到协程的并行。可以看到函数执行的总时间几乎等于其中最慢的协程的运行时间。第一种方案中把协程包了起来 asyncio.as_completed(coroutines) ,其实这里直接用 coroutines 也是可以执行的,不过那样的话协程之间就不是并行的了,而是串行执行了,整个函数的运行时间就是所有协程的时间之和。

  • as_completed 的方式:协程一旦运行结束,马上处理结果
  • gather 的方式:所有协程完成后统一返回结果并处理 
    as_completed 的方式灵活性更大,它也可以稍作修改,变成 gather 的方式(把所有结果保存起来,最后再处理,只不过暂时保存起来的过程交给用户处理了,而gather则是由框架代为处理)。还不知道 gather 内部的实现方式是否用到了 as_completed , 暂时在这里先留个问题,以后再来回答 。

tornado

python2.7及以上

tornado的代码就简短很多,直接yield一个coroutine的列表出去就好了:

import random
import time
from tornado import gen
from tornado.ioloop import IOLoop


@gen.coroutine
def get_url(url):
wait_time = random.randint(1, 4)
yield gen.sleep(wait_time)
print('URL {} took {}s to get!'.format(url, wait_time))
raise gen.Return((url, wait_time))


@gen.coroutine
def process_once_everything_ready():
before = time.time()
coroutines = [get_url(url) for url in ['URL1', 'URL2', 'URL3']]
result = yield coroutines
after = time.time()
print(result)
print('total time: {} seconds'.format(after - before))

if __name__ == '__main__':
IOLoop.current().run_sync(process_once_everything_ready)

输出:

$ python3 tornado_test.py
URL URL2 took 1s to get!
URL URL3 took 1s to get!
URL URL1 took 4s to get!
[('URL1', 4), ('URL2', 1), ('URL3', 1)]
total time: 4.000649929046631 seconds

在这里,总的运行时间也是等于最长的协程的运行时间,合理!tornado目前我只了解这种结果到了统一处理的写法,以后随着框架的不断学习,将会不断更新这篇文章。也非常希望大家给我留言补充。

当然因为现在tornado已经集成了 asyncio 以及 twisted 模块,也可以利用它们的方式去做,这里就不展开了。

总结

  • 在协程框架中的sleep,都不能用原来 time 模块中的sleep了,不然它会阻塞整个线程,而所有协程都是运行在同一个线程中的。可以看到两个框架都会sleep作了封装 gen.sleep() 和asyncio.sleep() ,内部的实现上,它们都是注册了一个定时器在eventloop中,把CPU的控制权交给其它协程。
  • 从协程的实现原理层面去说,也是比较容易理解这种并行方式的。两个框架都是把一个生成器对象的列表yield出去,交给调度器,再由调度器分别执行并注册回调,所以才能够实现并行。
posted @ 2017-03-28 21:38  众里寻,阑珊处  阅读(614)  评论(0编辑  收藏  举报
返回顶部