asyncio
模块提供了使用协程构建并发应用的工具。它使用一种单线程单进程的方式实现并发,应用的各个部分彼此合作,可以显示的切换任务,一般会在程序阻塞I/O操作的时候发生上下文切换如等待读写文件,或者请求网络。同时asyncio
也支持调度代码在将来的某个特定事件运行,从而支持一个协程等待另一个协程完成,以处理系统信号和识别其他一些事件。
对于其他的并发模型大多数采用的都是线性的方式编写。并且依赖于语言运行时系统或操作系统底层的底层线程或进程来适当的改变上下文,而基于asyncio
的应用要求应用代码显示的处理上下文切换。asyncio
提供的框架以事件循环为中心,程序开启一个无限的循环,程序会把一些函数注册到事件循环当中,当满足事件发生的时候,调用相应的协程函数。
使用asyncio
也就意味着你需要一直写异步方法。一个标准方法是这样的:
def regular_double(x):
return 2 * x
而一个异步方法:
async def async_double(x):
return 2 * x
从外观上来看,异步方法和标准方法没什么区别,只是前面多加了一个async
。async
是asynchronous
的简写,为了区别于异步函数,我们称标准函数为同步函数。要调用异步函数需要在前面增加一个await
关键字,但是不能在同步函数中使用await
,否则会报错,需在异步函数中使用!
错误写法:
def print_result():
print(await async_double(3))
正确写法:
async print_result():
print(await async_double(3))
启动一个协程
一般异步方法被称之为协程(Coroutine)。asyncio
事件循环可以通过多种不同的方法启动一个协程。如下:
async def print_message():
print("这是一个协程")
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
print("开始运行协程")
coro = print_message()
print("进入事件循环")
loop.run_until_complete(coro)
finally:
print("关闭事件循环")
loop.close()
输出结果如下:
开始运行协程
进入事件循环
这是一个协程
关闭事件循环
这是一个协程的简单例子:第一步首先获得一个事件循环的应用就是定义的对象loop。可以使用默认的事件循环,也可以实例化一个特定的循环类(比如uvloop),这里使用了默认的循环loop.run_until_complete(coro)
方法用这个协程启动这个循环,协程返回时这个方法将停止循环。run_until_complete
的参数是一个fetrue
对象,当传入一个协程,其内部会自动封装成一个task,其中task是fetrue的子类
从协程中返回值
我们将上面的代码进行修改,如下:
async def print_message():
print("这是一个协程")
return "协程返回值"
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
print("开始运行协程")
coro = print_message()
print("进入事件循环")
result = loop.run_until_complete(coro)
print(f"result: {result}")
finally:
print("关闭事件循环")
loop.close()
输出结果如下:
开始运行协程
进入事件循环
这是一个协程
result: 协程返回值
关闭事件循环
run_until_complete
可以获取协程的返回值,如果没有给定返回值,就像普通函数一样,默认返回None
协程调用协程
一个协程启动另外一个协程,从而可以任务根据工作内容封装到不同的协程当中,我们可以使用await
关键字,链式的调度协程,来形成一个协程任务流。如下:
async def main():
print("这是一个主协程")
print("等待coroutine_1执行")
result_1 = await coroutine_1()
print("等待coroutine_2执行")
result_2 = await coroutine_2(result_1)
return result_1, result_2
async def coroutine_1():
print("这是coroutine_1协程")
return "coroutine_1"
async def coroutine_2(args):
print("这是coroutine_2协程")
return f"coroutine_1 + {args}"
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
result = loop.run_until_complete(main())
print(f"result: {result}")
finally:
print("关闭事件循环")
loop.close()
输出结果如下:
这是一个主协程
等待coroutine_1执行
这是coroutine_1协程
等待coroutine_2执行
这是coroutine_2协程
result: ('coroutine_1', 'coroutine_1 + coroutine_1')
关闭事件循环
在协程中可以通过一些方法去调用普通的函数。可以使用的关键字有call_soon
、call_later
和call_at
call_soon
call_soon(callback, *args, context=None)
在下一个迭代的事件循环中立刻调用回调函数,大部分的回调函数都支持位置参数,而不支持“关键字参数”,如果想要使用关键字参数,则推荐使用functools.partial()
方法来进行包装。请看下面例子:
import asyncio
import functools
def call_back(*args, **kwargs):
print(f"回调函数的参数, args: {args}, kwargs: {kwargs}")
async def main(loop):
print("注册callback")
loop.call_soon(call_back, 1)
wrapper = functools.partial(call_back, name="laozhang")
loop.call_soon(wrapper, 1)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
print("关闭事件循环")
loop.close()
输出结果如下:
注册callback
回调函数的参数, args: (1,), kwargs: {}
回调函数的参数, args: (1,), kwargs: {'name': 'laozhang'}
关闭事件循环
有时,我们不想立即调用一个函数,此时我们就可以调用call_later
延时去调用一个函数了
call_soon_threadsafe
call_soon_threadsafe(callback, *args)
类似call_soon
,但是线程安全
call_later
call_later(delay, callback, *args, context=None)
首先简单说一下它的含义,就是事件循环在delay
多长时间之后才执行callback
函数,配合上面的call_soon
让我们看一个小例子:
import time
import asyncio
def call_back(*args, **kwargs):
t = time.time()
print(f"args: {args}, time: {t}")
async def main(loop):
print("注册call_back", time.time())
loop.call_later(1, call_back, "process-1")
loop.call_later(2, call_back, "process-2")
loop.call_soon(call_back, "process-3")
await asyncio.sleep(5)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
输出结果如下:
注册call_back 1584685555.1941936
args: ('process-3',), time: 1584685555.1951926
args: ('process-1',), time: 1584685556.1960201
args: ('process-2',), time: 1584685557.196542
从输出结果可以看出,call_soon
是立即执行的,两个call_later
是并发执行的!如果没有最后的await asyncio.sleep(5)
,那么两句call_later
将不会执行,只会执行call_soon
!
call_soon
会在call_later
之前执行,和它的位置在哪无关,call_later
的第一个参数越小,越先执行!
call_at
call_at(when, call_back, context=None)
call_at
第一个参数的含义代表的是一个单调时间,它和我们平时说的系统时间有点差异,这里的时间指的是时间循环内部的时间,可以通过loop.time()
获取,然后可以在此基础上进行操作。后面的参数和前面的两个方法一样,实际上call_later
内部就是调用的call_at
import asyncio
def call_back(num, loop):
print(f"call_back函数, num: {num}, time: {loop.time()}")
async def main(loop):
now = loop.time()
print("注册call_back函数", now)
loop.call_at(now + 1, call_back, 1, loop)
loop.call_at(now + 2, call_back, 2, loop)
loop.call_soon(call_back, 3, loop)
await asyncio.sleep(5)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
输出结果如下:
注册call_back函数 242044.906
call_back函数, num: 3, time: 242044.906
call_back函数, num: 1, time: 242045.906
call_back函数, num: 2, time: 242046.906
获取Future的结果
future
表示还没有完成的工作结果,事件循环可以通过监视一个future
对象的状态来指示它已经完成。future
对象有几个状态:
创建Future
的时候,task
为pending
,事件循环调用执行的时候自然就是running
,调用完毕自然就是done
。如果要停止事件循环,就需要先把task
取消,状态为cancel
import asyncio
def call_back(future, result):
print(f"进入call_back函数, future: {future}, result: {result}")
future.set_result(result)
print(f"此时future的结果为: {future}")
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
future = asyncio.Future()
loop.call_soon(call_back, future, "result is finished")
result = loop.run_until_complete(future)
print(f"run_until_complete, result: {result}")
finally:
loop.close()
输出结果如下:
进入call_back函数, future: , result: result is finished
此时future的结果为:
run_until_complete, result: result is finished
通过输出结果可以发现,调用set_result
之后,对象的状态会由pending
变为finished
,future
的实例会保留提供给方法的结果,可以在后续使用。
future对象使用await
future
可以像协程一样使用await
关键字获取结果
import asyncio
def call_back(future, result):
print(f"进入call_back函数, future: {future}, result: {result}")
future.set_result(result)
async def main(loop):
print(f"进入main异步函数")
future = asyncio.Future()
loop.call_soon(call_back, future, "result is finished")
print(await future)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
输出结果如下:
进入main异步函数
进入call_back函数, future: ()]>, result: result is finished
result is finished
future回调
future
在完成的时候可以执行一些回调函数,回调函数按照注册时的顺序进行调用
import asyncio
import functools
def call_back(future, n):
print(f"进入call_back函数, future: {future}, n: {n}")
async def register_callbacks(future):
print("注册call_back")
future.add_done_callback(functools.partial(call_back, n=1))
future.add_done_callback(functools.partial(call_back, n=2))
async def main(future):
await register_callbacks(future)
print("设置future的结果")
future.set_result("result is finished")
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
future = asyncio.Future()
loop.run_until_complete(main(future))
finally:
loop.close()
输出结果如下:
注册call_back
设置future的结果
进入call_back函数, future: , n: 1
进入call_back函数, future: , n: 2
通过add_done_callback
方法给future
添加回调函数,当future
执行完成的时候,就会调用回调函数。并通过future
获取协程执行的结果
任务(Task)是与事件循环交互的主要途径条件之一。任务可以包装成为协程,可以跟踪协程何时完成,任务是Future
的子类,所以使用方法和Future
一样。协程可以等待任务,每个任务都有一个结果,在它完成之后可以获取这个结果。因为协程是无状态的,我们通过使用create_task
方法,可以将协程包装成有状态的任务。还可以在任务运行的过程中取消任务。
import asyncio
async def child():
print("进入child协程")
return "result is ok!"
async def main(loop):
print("进入main协程")
task = loop.create_task(child())
print("cancel task")
task.cancel()
try:
await task
except asyncio.CancelledError:
print("取消任务抛出CancelledError")
else:
print("任务的结果: ", task.result())
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
输出结果如下:
进入main协程
cancel task
取消任务抛出CancelledError
如果我们把上面的task.cancel()
注释掉,那么我们就可以获得正常请看下的结果了,如下:
进入main协程
cancel task
进入child协程
任务的结果: result is ok!
同样,我们可以使用await
关键字来获取任务的结果!
res = await task
除了create_task
,我们还可以使用asyncio.ensure_future(coroutine)
创建一个task
一系列的协程可以通过await
链式的调用,但是有的时候我们需要在一个协程里等待多个协程,比如我们在一个协程里等待1000多个异步网络请求,对于访问次序没有要求的时候,就可以使用另外的关键字await
或gather
来解决了。await
可以暂停一个协程,直到后台操作完成。
等待多个协程
import asyncio
async def child(n):
print("进入child协程")
try:
await asyncio.sleep(n * 0.1)
return n
except asyncio.CancelledError:
print("数字{n}被取消".format(n=n))
raise
async def main():
tasks = [child(i) for i in range(10)]
complete, pending = await asyncio.wait(tasks, timeout=0.5)
print("complete", complete)
print("pending", pending)
for task in complete:
print("当前数字为: {result}".format(result=task.result()))
for task in pending:
task.cancel()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
complete { result=4>, result=0>, result=3>, result=1>, result=5>, result=2>}
pending { wait_for=()]>>, wait_for=()]>>, wait_for=()]>>, wait_for=()]>>}
当前数字为: 4
当前数字为: 0
当前数字为: 3
当前数字为: 1
当前数字为: 5
当前数字为: 2
数字7被取消
数字6被取消
数字8被取消
数字9被取消
可以发现我们的结果并没有按照数字的顺序显示,在内部await()
使用一个set()
保存它创建的Task
实例。因为set
是无序的,所以这也就是我们的任务没有按顺序执行的原因。await
的返回值是一个元组,包括两个集合,分别表示已完成任务和未完成任务。wait
函数中timeout
参数表示超时值,达到这个超时时间之后,未完成的任务状态变为pending
,当程序退出时还有任务没有完成时,就会看到如下的错误提示:
Task was destroyed but it is pending!
task: wait_for=()]>>
Task was destroyed but it is pending!
task: wait_for=()]>>
Task was destroyed but it is pending!
task: wait_for=()]>>
Task was destroyed but it is pending!
task: wait_for=()]>>
我们可以通过迭代调用cancel
方法来取消任务,也就是如下这段代码:
for task in pending:
task.cancel()
gather
的作用和wait
类似,但不同的是:
我们将上面的代码进行修改一些,使用gather
的方式:
import asyncio
async def child(n):
print("进入child协程")
try:
await asyncio.sleep(n * 0.1)
return n
except asyncio.CancelledError:
print("数字{n}被取消".format(n=n))
raise
async def main():
tasks = [child(i) for i in range(10)]
complete = await asyncio.gather(*tasks)
print(complete)
for result in complete:
print("当前数字为: {result}".format(result=result))
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
进入child协程
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
当前数字为: 0
当前数字为: 1
当前数字为: 2
当前数字为: 3
当前数字为: 4
当前数字为: 5
当前数字为: 6
当前数字为: 7
当前数字为: 8
当前数字为: 9
gather
通常被用来阶段性的一个操作,做完第一步才能做第二步,比如下面操作:
import asyncio
import time
async def child_1(timeout, now):
await asyncio.sleep(timeout)
print("第一阶段完成, 需时: {}".format(time.time() - now))
return timeout
async def child_2(timeout, now):
await asyncio.sleep(timeout)
print("第二阶段完成, 需时:{}".format(time.time() - now))
return timeout
async def main():
now = time.time()
result_list = await asyncio.gather(child_1(5, now), child_2(2, now))
for result in result_list:
print("result: {}".format(result))
print("总需时: {}".format(time.time() - now))
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
第二阶段完成, 需时:2.0005438327789307
第一阶段完成, 需时: 5.0014026165008545
result: 5
result: 2
总需时: 5.0014026165008545
通过上面的输出结果可以得到以下结论:
任务完成时进行处理
as_completed
方法返回一个生成器,会管理一个指定的任务列表,并生成它们的结果。每个协程结束运行时一次生成一个结果,与wait
一样,as_completed
不能保证顺序,不过执行其他动作之前没有必要等待所有后台操作完成
import asyncio
async def foo(n):
print("Waiting: ", n)
await asyncio.sleep(2)
return n
async def main():
tasks = [asyncio.ensure_future(foo(i)) for i in range(10)]
result_list = asyncio.as_completed(tasks)
print("result_list", result_list)
for result in result_list:
print("Task Res: {}, Content: {}".format(result, await result))
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
输出结果如下:
result_list
Waiting: 0
Waiting: 1
Waiting: 2
Waiting: 3
Waiting: 4
Waiting: 5
Waiting: 6
Waiting: 7
Waiting: 8
Waiting: 9
Task Res: ._wait_for_one at 0x0000013D1FF36360>, Content: 0
Task Res: ._wait_for_one at 0x0000013D1FF363B8>, Content: 2
Task Res: ._wait_for_one at 0x0000013D1FF36360>, Content: 6
Task Res: ._wait_for_one at 0x0000013D1FF363B8>, Content: 9
Task Res: ._wait_for_one at 0x0000013D1FF36360>, Content: 8
Task Res: ._wait_for_one at 0x0000013D1FF363B8>, Content: 5
Task Res: ._wait_for_one at 0x0000013D1FF36360>, Content: 7
Task Res: ._wait_for_one at 0x0000013D1FF363B8>, Content: 4
Task Res: ._wait_for_one at 0x0000013D1FF36360>, Content: 1
Task Res: ._wait_for_one at 0x0000013D1FF363B8>, Content: 3