asyncio vs gevent

前言

本文列举了一些简单场景下的协程应用,主要目的是通过对比asynciogevent的API设计,加深对协程的理解,同时了解这两种协程实现在用法上的特点和差异。

环境及版本

  • Python 3.7

一、创建简单协程并发任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#  使用asyncio完成简单协程并发任务
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
# 使用gevent完成简单协程并发任务
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
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实现的协程,代码的执行逻辑并不直观,还需要进行实际的测试才能更好的理解。

二、GreenletTask方法对比

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()添加的回调函数

二、geventasyncio协程调度函数对比

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设计非常相似,同样的功能用它们两个都可以实现,只不过是代码风格上会有些差异。