协程
协程(coroutines)是通过async/await
定义函数或方法,是使用asyncio进行异步编程的首选途径。如下,是一个协程的例子:
1
|
import asyncio
|
上例中的 main
方法就是我们定义的协程
。代码的功能很简单:
我们在交互环境(Python3.7)下执行以上代码,看看效果:
1
|
>>> import asyncio
|
需要注意的是:如果像执行普通代码一样直接调用main()
,只会返回一个coroutine
对象,main()
方法内的代码不会执行:
1
|
|
实际上,asyncio提供了三种执行协程
的机制:
- 使用
asyncio.run()
执行协程。一般用于执行最顶层的入口函数,如main()
。 await
一个协程
。一般用于在一个协程
中调用另一协程
。 如下是一个示例:
1
|
|
执行耗时 3秒
- 用
asyncio.create_task()
方法将Coroutine(协程)
封装为Task(任务)
。一般用于实现异步并发操作。 需要注意的是,只有在当前线程存在事件循环的时候才能创建任务(Task)。
我们修改以上的例程,并发执行 两个say_after
协程。
1
|
async def main():
|
执行asyncio.run(main())
,结果如下:
1
|
started at 17:01:34
|
耗时2秒
“可等待”对象(Awaitables)
如果一个对象能够被用在await
表达式中,那么我们称这个对象是可等待对象(awaitable object)
。很多asyncio API
都被设计成了可等待的
。
主要有三类可等待对象:
- 协程
coroutine
- 任务
Task
- 未来对象
Future
。
Coroutine(协程)
Python的协程
是可等待的(awaitable)
,因此能够被其他协程
用在await
表达式中。
1
|
import asyncio
|
重要:在这篇文章中,术语coroutine
或协程
指代两个关系紧密的概念:
协程函数(coroutine function)
:由async def
定义的函数;协程对象(coroutine object)
:调用协程函数
返回的对象。
asyncio也支持传统的基于生成器的协程。
Task(任务)
Task
用来 并发的 调度协程。
当一个协程
通过类似 asyncio.create_task()
的函数被封装进一个 Task
时,这个协程
会很快被自动调度执行:
1
|
import asyncio
|
Future(未来对象)
Future
是一种特殊的 底层 可等待对象,代表一个异步操作的最终结果。
当一个Future
对象被await
的时候,表示当前的协程会持续等待,直到 Future
对象所指向的异步操作执行完毕。
在asyncio中,Future
对象能使基于回调的代码被用于asyn/await
表达式中。
一般情况下,在应用层编程中,没有必要 创建Future
对象。
有时候,有些Future
对象会被一些库和asyncio API暴露出来,我们可以await
它们:
1
|
async def main():
|
底层函数返回Future
对象的一个例子是:loop.run_in_executor
执行asyncio程序
1
|
asyncio.run(coro, * , debug=False)
|
这个函数运行coro
参数指定的 协程
,负责 管理asyncio事件循环 , 终止异步生成器。
在同一个线程中,当已经有asyncio事件循环在执行时,不能调用此函数。
如果debug=True
,事件循环将运行在 调试模式。
此函数总是创建一个新的事件循环,并在最后关闭它。建议将它用作asyncio程序的主入口,并且只调用一次。
Python3.7新增
重要:这个函数是在Python3.7被临时添加到asyncio中的。
创建Task
1
|
asyncio.create_task(coro)
|
将coro
参数指定的协程(coroutine)
封装到一个Task
中,并调度执行。返回值是一个Task
对象。
任务在由get_running_loop()
返回的事件循环(loop)中执行。如果当前线程中没有正在运行的事件循环,将会引发RuntimeError
异常:
1
|
import asyncio
|
因为当前线程中没有正运行的事件循环,所以引发异常:
1
|
Traceback (most recent call last):
|
对以上代码稍作修改,创建main()
方法,在其中创建Task
对象,然后在主程序中利用asyncio.run()
创建事件循环
:
1
|
import asyncio
|
执行结果如下:
1
|
<_WindowsSelectorEventLoop running=True closed=False debug=False>
|
此函数已经被引入到Python3.7。在Python早期版本中,可以使用底层函数asyncio.ensure_future()
代替。
1
|
async def coro():
|
Python3.7新增
Sleeping
coroutine asyncio.sleep(delay,result=None,* ,loop=None)
阻塞delay
秒,例如delay=3
,则阻塞3秒。
如果指定了result
参数的值
,则在协程结束时,将该值
返回给调用者。sleep()
通常只暂停当前task
,并不影响其他task
的执行。
不建议使用loop
参数,因为Python计划在3.10
版本中移除它。
以下是一个协程的例子,功能是在5秒钟内,每秒显示一次当前的日期
:
1
|
import asyncio
|
执行结果大致如下:
1
|
2018-11-20 11:27:15.961830
|
并发执行Tasks
awaitable asyncio.gather(* aws, loop=None, return_exceptions=False)
并发执行aws
参数指定的 可等待(awaitable)对象
序列。
如果 aws
序列中的某个 awaitable 对象
是一个 协程
,则自动将这个 协程
封装为 Task
对象进行处理。例如:
1
|
import asyncio
|
如果所有的awaitable
对象都执行完毕,则返回 awaitable对象执行结果的聚合列表。返回值的顺序于aws
参数的顺序一致。
简单修改以上代码:
1
|
import asyncio
|
如果return_execptions
参数为False
(默认值即为False
),引发的第一个异常会立即传播给等待gather()
的任务,即调用await asyncio.gather()
对象。序列中其他awaitable
对象的执行不会受影响。例如:
1
|
import asyncio
|
执行结果:
1
|
2/1=2.0
|
如果return_exceptions
参数为True
,异常会和正常结果一样,被聚合到结果列表中返回。
对以上代码稍作修改,将return_exceptions
设为True
:
1
|
import asyncio
|
执行结果如下:
1
|
2/1=2.0
|
如果gather()
被取消,则提交的所有awaitable
对象(尚未执行完成的)都会被取消。例如:
1
|
import asyncio
|
执行结果:
1
|
5/1=5.0 #除已执行的之外,其他的任务全部被取消
|
如果aws
中某些Task
或Future
被取消,gather()
调用不会被取消,被取消的Task
或Future
会以引发CancelledError
的方式被处理。这样可以避免个别awaitable
对象的取消操作影响其他awaitable
对象的执行。
例如:
1
|
import asyncio
|
预期执行结果如下:
1
|
5/1=5.0
|
避免取消
awaitable asyncio.shield(aw, * , loop=None)
防止awaitable
对象被取消(cancelled)执行。
如果aw
参数是一个协程(coroutines)
,该对象会被自动封装为Task
对象进行处理。
通常,代码:
#code 1
res = await shield(something())
同代码:
#code 2
res = await something()
是等价的。
特殊情况是,如果包含以上代码的协程
被 取消,code 1
与code 2
的执行效果就完全不同了:
code 1
中,运行于something()
中的任务 不会被取消。code 2
中,运行于something()
中的任务 会被取消。
在code 1
中,从something()
的视角看,取消操作并没有发生。然而,事实上它的调用者确实被取消了,所以await shield(something())
仍然会引发一个CancelledError
异常。
1
|
import asyncio
|
执行结果:
1
|
Start time:10:38:48
|
如果something()
以其他的方式被取消,比如从自身内部取消,那么shield()
也会被取消。
如果希望完全忽略取消操作
(不推荐这么做),则可以将shield()
与try/except
结合起来使用:
1
|
try:
|
超时(Timeouts)
1
|
coroutine asyncio.wait_for(aw,timeout,*,loop=None)
|
在timeout
时间之内,等待aw
参数指定的awaitable
对象执行完毕。
如果aw
是一个协程,则会被自动作为Task
处理。timeout
可以是None
也可以是一个float
或int
类型的数字,表示需要等待的秒数。如果timeout
是None
,则永不超时,一直阻塞到aw
执行完毕。
如果达到timeout
时间,将会取消待执行的任务,引发asyncio.TimeoutError
.
如果想避免任务被取消,可以将其封装在shield()
中。
程序会等待到任务确实被取消掉,所以等待的总时间会比timeout
略大。
如果await_for()
被取消,aw
也会被取消。loop
参数将在Python3.10中删除,所以不推荐使用。
示例:
1
|
async def eternity():
|
Python3.7新特性:当aw
因为超时被取消,wait_for()
等到aw
确实被取消之后返回异常。在以前的版本中,wait_for
会立即返回异常。
等待原语(Waiting Primitives)
wait()
1
|
coroutine asyncio.wait(aws,*,loop=None,timeout=None,return_when=ALL_COMPLETED)
|
并发执行aws
中的awaitable
对象,一直阻塞到return_when
指定的情况出现。
如果aws
中的某些对象是协程(coroutine)
,则自动转换为Task
对象进行处理。直接将coroutine
对象传递给wait()
会导致令人迷惑的执行结果,所以不建议这么做。
返回值是两个Task/Future
集合:(done,pending
)。
用法示例:
1
|
done,pending = await asyncio.wait(aws)
|
loop
参数将在Python3.10中删除,所以不建议使用。timeout
参数可以是一个int
或float
类型的值,可以控制最大等待时间。
需要注意的是,wait()
不会引发asyncio.TimeoutError
错误。返回前没有被执行的Future
和Task
会被简单的放入pending
集合。return_when
决定函数返回的时机。它只能被设置为以下常量:
Constant | Description |
---|---|
FIRST_COMPLETED | The function will return when any future finishes or is cancelled. |
FIRST_EXCEPTION | The function will return when any future finishes by raising an exception. If no future raises an exception then it is equivalent to ALL_COMPLETED. |
ALL_COMPLETED | The function will return when all futures finish or are cancelled. |
与wait_for()
不同,wait()
不会再超时的时候取消任务。
注意:
因为wait()
会自动将协程
转换为Task对象
进行处理,然后返回这些隐式创建的Task
到(done,pending)集合,所以以下代码不会如预期的那样执行。
1
|
async def foo():
|
上面的代码可以做如下修正:
1
|
async def foo():
|
所以,正如上文所讲,不建议将coroutine
对象直接传递给wait()
。
as_completed()
1
|
asyncio.as_completed(aws,*,loop=None,timeout=None)
|
并发执行aws
中的awaitable
对象。返回一个Future
对象迭代器。每次迭代时返回的Future
对象代表待执行的awaitable
对象集合里最早出现的结果。注意:迭代器返回的顺序与aws
列表的顺序无关,只与结果出现的早晚有关。
如果超时之前还有Future
对象未完成,则引发asyncio.TimeoutError
异常。
用法示例:
1
|
for f in as_completed(aws):
|
以下为一个完整的例子:
1
|
import asyncio
|
执行结果如下:
1
|
Start at: 17:19:11
|
从其他线程调度执行(Scheduling From Other Threads)
1
|
asyncio.run_coroutine_threadsafe(coro,loop)
|
向loop
指定的事件循环提交一个由coro
指定协程。线程安全。
返回一个concurrent.futures.Future
对象,等待另一个线程返回结果。
这个函数用于从当前线程
向运行事件循环的线程
提交协程(coroutine)
。
例如:
1
|
# Create a coroutine
|
如果协程
出现异常,返回的Future
会收到通知。返回的Future
也可以被用作取消事件循环中的任务:
1
|
try:
|
可以参考并发与多线程章节。
与其他asyncio函数不同,该函数需要 显式 传递loop
参数。
新增于Python 3.5.1
自查(Introspection)
current_task()
1
|
asyncio.current_task(loop=None)
|
返回事件循环中正在运行的Task
实例,如果没有Task
在执行,则返回None
。
如果loop
为None
,则使用get_running_loop()
获取当前事件循环。
新增于Python3.7
all_tasks()
1
|
asyncio.all_tasks(loop=None)
|
返回事件循环中尚未运行结束的Task
对象集合。
如果loop
为None
,则,使用get_running_loop()
获取当前事件循环。
新增于Python3.7
Task对象
1
|
class asyncio.Task(coro,*,loop=None)
|
类似与Future
对象,用于执行Python协程。非线程安全。Tasks
用于在事件循环
中执行协程
。如果协程
等待一个Future
,那么Task
会暂停协程
的执行,直到Future
执行完成。当Future
完成时,协程
的执行会恢复。
事件循环的 协作调度 模式:一个事件循环同一时间只执行一个Task
。当这个Task
等待某个Future
返回时,事件循环执行其他的Task
、回调
或IO操作
。
可以通过高层函数asyncio.create_task()
创建Task
,或者通过底层函数loop.create_task()
和ensure_future()
创建Task
。但是不建议直接实例化Task
对象。
如果想要取消一个Task
的执行,可以使用cancel()
方法。调用cancel()
会引起Task
对象向被封装的协程
抛出CancelledError
异常。当取消行为发生时,如果协程
正在等待某个Future
对象执行,该Future
对象将被取消。
cancelled()
方法用于检查某个Task
是否已被取消。如果Task
封装的协程
没有阻止CancelledError
异常,且Task
确实被取消了,则该方法返回True
。
asyncio.Task
继承了Future
类中除Future.set_result()
和Future.set_exception()
以外的所有方法。
Task
对象支持contextvars
模块:当一个Task
被创建的时候,它会复制当前的上下文,然后在复制的上下文副本中执行协程。
Python3.7中的变更:添加了对contextvars
模块的支持。
cancel()
申请取消任务。
将在下一个事件循环周期中将CancelledError
异常抛给封装在Task
中的协程。
收到CancelledError
异常后,协程
有机会处理异常,甚至以try ...except CancelledError ...finally
来拒绝请求。因此,与Future.cancel()
不同,Task.cancel()
不能保证Task
一定被取消掉。当然,拒绝取消请求这种操作并不常见,而且很不提倡。
以下例子可以说明协程如何拦截取消请求:
1
|
import asyncio
|
cancelled()
如果Task
已经被取消,则返回True
。
当取消请求通过cancel()
被提交,且Task
封装的协程
传播了抛给它的CancelledError
异常,则此Task
被取消。
done()
如果Task
已完成,则返回True
。Task
完成有三种情况:
- 封装的协程已返回
- 封装的协程已抛出异常
Task
被取消
result()
返回Task
的执行结果。
如果Task
已经完成,则返回Task
封装的协程的执行结果(如果Task
封装的协程引发异常,则重新引发该异常)。
如果Task
已经取消,则该方法引发CancelledError
异常。
如果Task
的结果还不可用,该方法引发InvalidStateError
异常。
exception()
返回Task
的异常。
如果封装的协程引发了异常,则返回此异常。如果封装的协程执行正常,则返回None
。
如果Task
已被取消,则引发CancelledError
异常。
如果Task
尚未完成,则引发InvalidStateError
异常。
add_done_callback()
添加一个回调函数,在Task
完成后执行。
这个方法只应用在基于回调的底层编程中。
具体细节可以参考Future.remove_done_callback()
get_stack(* ,limit=None)
返回此Task
的堆栈帧列表。
- 如果封装的协程未完成,此方法返回它暂停位置的堆栈。
- 如果封装的协程已经完成或已被取消,此方法返回一个空的列表。
- 如果封装的协程因异常而结束,此方法返回异常回溯列表。
帧的顺序总是 由旧到新。
暂停中的协程只返回一个堆栈帧。
可选参数limit
用于限定返回帧的最大数目。默认情况下,所有有效的帧都会返回。
在返回堆栈和返回异常回溯时,列表的顺序会有所不同:
- 最新的堆栈帧会被返回
- 最老的回溯帧会被返回(这和异常回溯模块的机制有关)
print_stack(* ,limit=None,file=None)
打印Task
的栈帧或异常回溯。
此方法用于输出由get_stack()
取回的帧列表,输出形式类似于回溯(traceback)模块
limit
参数会直接传递给get_stack()
。file
参数指定输出的I/O流,默认为sys.stderr
。
classmethod all_tasks(loop=None)
返回一个事件循环上所有任务的集合。
默认情况下,当前事件循环上所有的任务都会被返回。如果loop
参数为’None’,则通过get_event_loop()
方法获取当前事件循环。
此方法将在Python3.9中被移除,所以不建议使用。可以使用asyncio.all_tasks()
代替。
calssmethod current_task(loop=None)
返回当前正在运行的Task
或None
。
如果loop
参数为’None’,则通过get_event_loop()
方法获取当前事件循环。
此方法将在Python3.9中被移除,所以不建议使用。可以使用asyncio.current_task()
代替。
基于生成器的协程(Generator-based Coroutines)
提示:对基于生成器的协程的支持将在Python3.10中移除,不建议使用。
基于生成器的协程是早期的异步实现方式,出现在async/await
语法之前,使用yield from
表达式等待Future
或其他协程。
基于生成器的协程应该用`@asyncio.coroutine`来修饰,尽管这不是强制的。
@asyncio.coroutine
基于生成器的协程的修饰器。
这个修饰器能使传统的基于生成器的协程
与async/await
语法兼容:
1
|
|
此修饰器将在Python3.10中被移除,所以不建议再使用。
此修饰器不能用于async def
的协程中。
asyncio.iscoroutine(obj)
如果obj
对象是一个coroutine
对象,则返回True
。
此方法与inspect.iscoroutine()
不同,因为它对基于生成器的协程也返回True
。
asyncio.iscoroutinefunction(func)
如果func
是一个coroutine
方法,则返回True
。
此方法inspect.iscoroutinefunction()
不同,因为它对用@coroutine
修饰的基于生成器的协程也返回True
。