前言
本文列举了一些简单场景下的协程应用,主要目的是通过对比asyncio
和gevent
的API设计,加深对协程的理解,同时了解这两种协程实现在用法上的特点和差异。
环境及版本
一、创建简单协程并发任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import asyncio import aiohttp
async def download(url): async with aiohttp.ClientSession() as session: async with session.get(url) as response: content = await response.read() return content
tasks = list() url_list = ["http://www.xxx.com"]*10 loop = asyncio.get_event_loop() for url in url_list: task = loop.create_task(download(url)) tasks.append(task) result = loop.run_until_complete(asyncio.gather(*tasks)) loop.close()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import gevent from gevent import monkey monkey.patch_all() import requests
def download(url): with requests.session() as session: response = session.get(url) return response.content
tasks = list() url_list = list() for url in url_list: greenlet_coro = gevent.spawn(download, url) tasks.append(greenlet_coro)
gevent.joinall(tasks)
|
对比来看,在原生协程中,await
语句声明了一个可以挂起当前协程的断点,异步等待一个awaitable
对象,同时切换到其他可执行的协程,配合I/O多路复用实现了单线程的并发效果;而Gevent则是通过Monkey Patch,修改了socket
的行为,将其转变为非阻塞模式,当然同样是以I/O多路复用为基础。对比来看,这两段简单代码的逻辑也是非常相似,只不过asyncio需要显式声明事件循环,而Gevent并不需要。
当然还有一点需要注意的是,monkey.patch_all()
打上猴子补丁的位置是有讲究的,官方文档明确提到的,打补丁要在尽量靠前的位置,同时也要考虑并发编程时在什么位置更合适,不过实际的gevent实现的协程,代码的执行逻辑并不直观,还需要进行实际的测试才能更好的理解。
二、Greenlet
和Task
方法对比
class 'Task'
method |
描述 |
cancel() |
取消协程,抛出CancelledError 异常 |
cancelled() |
task是否被取消 |
done() |
task是否已完成 |
result() |
返回协程中return的对象 |
exception() |
返回task抛出的异常 |
add_done_callback(callback, *, context=None) |
为task添加回调函数 |
remove_done_callback(callback) |
移除task的回调函数 |
这里推荐使用Python 3.7版本的asyncio并且阅读对应的官方标准库文档,3.7的文档重新整理了接口分类,指明了哪些是high level api,哪些是low level API,为使用提供了一些实际的指导。同时也修复了一些在3.6版本中显而易见的bug。
class 'Greenlet'
method |
描述 |
ready() |
greenlet是否执行完成 |
successful() |
greenlet是否执行完成且没有抛出异常 |
start() |
立即调度greenlet |
start_later(seconds) |
延时调度greenlet |
join(timeout=None) |
等待greenlet结束或超时 |
kill(exception=GreenletExit, block=True, timeout=None) |
在greenlet中抛出异常 |
link(callback) |
添加一个greenlet生命周期结束时的回调函数 |
link_value(callback) |
添加一个greenlet完成,并正常返回时回调函数 |
link_exception(callback) |
添加一个greenlet完成,并抛出异常的回调函数 |
rawlink(callback) |
添加一个greenlet执行完成时的回调函数 |
unlink(callback) |
移除link() 或rawlink() 添加的回调函数 |
二、gevent
和asyncio
协程调度函数对比
asyncio
function |
描述 |
run(coro, *, debug=False) |
直接运行一个协程 |
create_task(coro) |
创建一个Task实例,用于调度协程函数 |
sleep(delay, result=None, *, loop=None) |
sleep |
gather(*aws, loop=None, return_exceptions=False) |
按照顺序并发调度多个awaitable对象,返回一个awaitable object |
wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED) |
并发调度awaitables,阻塞至满足return_when的条件,返回一个coroutine |
wait_for(aw, timeout, *, loop=None) |
等待一个awaitable完成或超时 |
as_completed(aws, *, loop=None, timeout=None) |
并发执行awaitable objects,返回包含Future的迭代器 |
run_coroutine_threadsafe(coro, loop) |
保证线程安全地执行一个协程 |
current_task(loop=None) |
获得当前正在运行的task |
all_tasks(loop=None) |
获得事件循环中所有未完成的task |
由于asyncio
需要显式声明事件循环,并把一部分功能交给了事件循环来做,所以这里把loop
的一些涉及到协程调度的方法也列出来:
method |
描述 |
run_until_complete(future) |
调度执行futurn,阻塞等待至其运行完成 |
call_soon(callback, *args, context=None) |
在事件循环的下一轮中调度callback |
call_later(delay, callback, *args, context=None) |
延迟调度callback |
create_task(coro) |
创建一个Task实例,用于调度协程函数 |
gevent
funtion |
描述 |
spawn(function, *args, **kwargs) |
创建一个greenlet对象,并调度它执行function |
spawn_later(seconds, function, *args, **kwargs) |
创建一个greenlet对象并延迟调度 |
spawn_raw(function, *args, **kwargs) |
和spawn 相似,详见文档描述 |
getcurrent() |
获取当前正在执行的greenlet |
kill(greenlet, exception=GreenletExit) |
异步终止greenlet |
killall(greenlets, exception=GreenletExit, block=True, timeout=None) |
通过使greenlet抛出异常来强制终止所有greenlet |
sleep(seconds=0, ref=True) |
sleep |
wait(objects=None, timeout=None, count=None) |
等待一个object处于ready状态或事件循环结束 |
iwait(objects, timeout=None, count=None) |
等待所有object就绪或到达超时事件,返回一个迭代器 |
joinall(greenlets, timeout=None, raise_error=False, count=None) |
等待所有greenlet完成 |
可以看到,整体来说,gevent和asyncio的api设计非常相似,同样的功能用它们两个都可以实现,只不过是代码风格上会有些差异。