目前 Python 语言的协程从实现来说可分为两类:
- 一种是基于传统生成器的协程,叫做 generator-based coroutines,通过包装 generator 对象实现。
- 另一种在 Python 3.5 版本 PEP 492 诞生,叫做 native coroutines,即通过使用
async
语法来声明的协程。
本文主要介绍第二种,第一种基于生成器的协程已在 Python 3.8 中弃用,并计划在 Python 3.10 中移除。本文是「介绍」,就先不讨论太多实现原理的东西,感兴趣的童鞋可以继续关注后面的文章。
协程(coroutine)
首先,来看一个非常简单的例子:
import asyncio
async def c():
await asyncio.sleep(1)
return 'Done '
这个被 async
修饰的函数 c
就是一个协程函数,该函数会返回一个协程对象。
In [1]: asyncio.iscoroutinefunction(c)
Out[1]: True
In [2]: c()
Out[2]: <coroutine object c at 0x107b0f748>
In [3]: asyncio.iscoroutine(c())
Out[3]: True
一般来说,协程函数 c
应具有以下特点:
- 一定会返回一个协程对象,而不管其中是否有
await
表达式。 - 函数中不能再使用
yield from
。 - 函数内部可通过
await
表达式来挂起自身协程,并等待另一个协程完成直到返回结果。 -
await
表达式后面可以跟的一定是一个可等待对象,而不仅仅是协程对象。 - 不可在
async
修饰的协程函数外使用await
关键字,否则会引发一个SyntaxError
。 - 当对协程对象进行垃圾收集时,如果从未等待过它,则会引发一个
RuntimeWarning
(如果你刚开始写 async,那你一定遇得到,不经意间就会忘掉一个await
)。
下面分别介绍下上面提到的两个概念,可等待对象和协程对象。
可等待对象(awaitable)
我们来看 collections.abc
模块中对 Awaitable
类的定义:
class Awaitable(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __await__(self):
yield
@classmethod
def __subclasshook__(cls, C):
if cls is Awaitable:
return _check_methods(C, "__await__")
return NotImplemented
可见,可等待对象主要实现了一个 __await__
方法。且该方法必须返回一个迭代器(iterator) ①,否则将会引发一个 TypeError
。
主要实现是因为
await
表达式需要跟老的基于生成器的协程相兼容,即通过使用types.coroutine()
或asyncio.coroutine()
装饰器返回的生成器迭代器对象(generator iterator)也属于可等待对象,但它们并未实现__await__
方法。
协程对象(Coroutine)
同样的,我们来看 collections.abc
模块中对 Coroutine
类的定义:
class Coroutine(Awaitable):
__slots__ = ()
@abstractmethod
def send(self, value):
"""Send a value into the coroutine.
Return next yielded value or raise StopIteration.
"""
raise StopIteration
@abstractmethod
def throw(self, typ, val=None, tb=None):
"""Raise an exception in the coroutine.
Return next yielded value or raise StopIteration.
"""
if val is None:
if tb is None:
raise typ
val = typ()
if tb is not None:
val = val.with_traceback(tb)
raise val
def close(self):
"""Raise GeneratorExit inside coroutine.
"""
try:
self.throw(GeneratorExit)
except (GeneratorExit, StopIteration):
pass
else:
raise RuntimeError("coroutine ignored GeneratorExit")
@classmethod
def __subclasshook__(cls, C):
if cls is Coroutine:
return _check_methods(C, '__await__', 'send', 'throw', 'close')
return NotImplemented
由上可知,由于继承关系,协程对象是属于可等待对象的。
除了协程 Coroutine
对象外,目前常见的可等待对象还有两种:asyncio.Task
和 asyncio.Future
,下文中介绍。
协程的执行可通过调用 __await__()
并迭代其结果进行控制。当协程结束执行并返回时,迭代器会引发 StopIteration
异常,并通过该异常的 value
属性来传播协程的返回值。下面看一个简单的例子:
In [4]: c().send(None)
Out[4]: <Future pending>
In [5]: async def c1():
...: return "Done "
...:
In [6]: c1().send(None)
Out[6]: StopIteration: Done
运行
协程的运行需要在一个 EventLoop 中进行,由它来控制异步任务的注册、执行、取消等。其大致原理是:
把传入的所有异步对象(准确的说是可等待对象,如
Coroutine
,Task
等,见下文)都注册到这个 EventLoop 上,EventLoop 会循环执行这些异步对象,但同时只执行一个,当执行到某个对象时,如果它正在等待其他对象(I/O 处理) 返回,事件循环会暂停它的执行去执行其他的对象。当某个对象完成 I/O 处理后,下次循环到它的时候会获取其返回值然后继续向下执行。这样以来,所有的异步任务就可以协同运行。
EventLoop 接受的对象必须为可等待对象,目前主要有三种类型即 Coroutine
, Task
和 Future。
下面简单的介绍下 Task
和 Future
:
-
Future
是一种特殊的低级的可等待对象,用来支持底层回调式代码与高层async/await
式的代码交互,是对协程底层实现的封装,其表示一个异步操作的最终结果。它提供了设置和获取Future
执行状态或结果等操作的接口。Future
实现了__await__
协议,并通过__iter__ = __await__
来兼容老式协程。一般来说,我们不需要关心这玩意儿,日常的开发也是不需要要用到它的。如有需要,就用其子类 Task。 - Task 用来协同的调度协程以实现并发,并提供了相应的接口供我们使用。
创建一个 Task
非常简单:
In [6]: loop = asyncio.get_event_loop()
In [7]: task = loop.create_task(c())
In [8]: task
Out[8]: <Task pending coro=<c() running at <ipython-input-1-3afd2bbb1944>:3>>
In [9]: task.done()
Out[9]: False
In [10]: task.cancelled()
Out[10]: False
In [11]: task.result()
Out[11]: InvalidStateError: Result is not set.
In [12]: await task
Out[12]: 'Done '
In [13]: task
Out[13]: <Task finished coro=<c() done, defined at <ipython-input-1-3afd2bbb1944>:3> result='Done '>
In [14]: task.done()
Out[14]: True
In [15]: task.result()
Out[15]: 'Done '
In [16]: task = loop.create_task(c())
In [17]: task.cancel()
Out[17]: True
In [18]: await task
Out[18]: CancelledError:
上面说到,协程的运行需要在一个 EventLoop 中进行,在 Python 3.7 之前,你只能这么写 :
In [19]: loop = asyncio.get_event_loop()
In [20]: loop.run_until_complete(c())
Out[20]: 'Done '
In [21]: loop.close()
Python 3.7 及以上版本可以直接使用 asyncio.run()
:
In [22]: asyncio.run(c())
Out[22]: 'Done '
并发
有些童鞋可能有疑问了,我写好了一个个协程函数,怎样才能并发的运行 ?
asyncio
提供了相应的两个接口:asyncio.gather
和 asyncio.wait
来支持:
async def c1():
await asyncio.sleep(1)
print('c1 done')
return True
async def c2():
await asyncio.sleep(2)
print('c2 done')
return True
async def c12_by_gather():
await asyncio.gather(c1(), c2())
async def c12_by_awit():
await asyncio.wait([c1(), c2()])
In [23]: asyncio.run(c12_by_gather())
c1 done
c2 done
In [24]: asyncio.run(c12_by_awit())
c1 done
c2 done
其它
上面我们介绍了 PEP 492 coroutine 的基础使用,同时 PEP 492 也相应提出了基于 async with
和 async for
表达式的异步上下文管理器(asynchronous context manager)和异步迭代器(asynchronous iterator)。
下面的介绍的示例将基于 Python 可迭代对象, 迭代器和生成器 里的示例展开,建议感兴趣的同学可以先看下这篇文章。
异步上下文管理器
在 Python 中,我们常会通过实现 __enter__()
和 __exit__()
方法来实现一个上下文管理器:
In [1]: class ContextManager:
...:
...: def __enter__(self):
...: print('enter...')
...:
...: def __exit__(slef, exc_type, exc_val, exc_tb):
...: print('exit...')
...:
In [2]: with ContextManager():
...: print('Do something...')
...:
enter...
Do something...
exit...
同样的,在异步编程时我们可以通过实现 __aenter__()
和 __aexit__()
方法来实现一个上下文管理器,并通过 async with
表达式来使用。
In [1]: class AsyncContextManager:
...:
...: async def __aenter__(self):
...: print('async enter...')
...:
...: async def __aexit__(slef, exc_type, exc_val, exc_tb):
...: print('async exit...')
...:
In [2]: async with AsyncContextManager():
...: print('Do something...')
...:
async enter...
Do something...
async exit...
异步迭代
在之前的文章 Python 可迭代对象, 迭代器和生成器 中,我们介绍了通过实现 __iter__()
方法来实现一个可迭代对象,通过实现迭代器协议 __iter__()
和 __next__()
方法来实现一个迭代器对象。下面我们改造下之前的例子,实现一个异步的版本。
class AsyncLinkFinder:
PATTERN = "(?<=href=").+?(?=")|(?<=href=').+?(?=')"
def __init__(self, text):
self.links = re.findall(self.PATTERN, text)
def __aiter__(self):
return AsyncLinkIiterator(self.links)
class AsyncLinkIiterator:
def __init__(self, links):
self.links = links
self.index = 0
async def _gen_link(self):
try:
link = self.links[self.index]
self.index += 1
except IndexError:
link = None
return link
def __aiter__(self):
return self
async def __anext__(self):
link = await self._gen_link()
if link is None:
raise StopAsyncIteration
return link
In [7]: async for s in AsyncLinkFinder(TEXT):
...: print(s)
https://blog.python.org
http://feedproxy.google.com/~r/PythonSoftwareFoundationNew/~3/T3r7qZxo-xg/python-software-foundation-fellow.html
http://feedproxy.google.com/~r/PythonSoftwareFoundationNews/~3/lE0u-5MIUQc/why-sponsor-pycon-2020.html
http://feedproxy.google.com/~r/PythonSoftwareFoundationNews/~3/jAMRqiPhWSs/seeking-developers-for-paid-contract.html
例子中实现了 __aiter__()
方法的 AsyncLinkFinder
就是一个异步可迭代对象,__aiter__()
方法返回的必须是一个异步迭代器,如 AsyncLinkIiterator
。异步迭代器必须同时实现 __aiter__()
和 __anext__()
方法。一个不同的点是,异步迭代中,当迭代器耗尽时,需要引发一个 StopAsyncIteration
而不是 StopIteration
。
同样的,我们也实现一个异步生成器版本的:
class LinkFinder:
PATTERN = "(?<=href=").+?(?=")|(?<=href=').+?(?=')"
def __init__(self, text):
self.links = re.finditer(self.PATTERN, text)
async def __aiter__(self):
return (link.group() for link in self.links)
注
- ① 关于迭代器的介绍可阅读 Python 可迭代对象, 迭代器和生成器。
- ② 关于示例中 __slots__ 的介绍请查看 理解 Python 类属性之 __slots__。
参考
- PEP 492: Coroutines with async and await syntax
- PEP 342: Coroutines via Enhanced Generators