34

深入理解协程(三):async/await实现异步协程

 4 years ago
source link: http://www.cnblogs.com/ghostlee/p/12190056.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

原创不易,转载请联系作者

深入理解协程 分为三部分进行讲解:

  • 协程的引入
  • yield from实现异步协程
  • async/await实现异步协程

本篇为 深入理解协程 系列文章的 最后一篇

从本篇你将了解到:

  1. async/await 的使用。
  2. 如何从 yield from 风格的协程修改为 async/await 风格。

篇幅较长,请耐心阅读。

async/await的引入

上篇 【yield from实现异步协程】 我们引入了 asynico 模块,结合 yield from 实现异步协程。但语法不够简洁,其中涉及的 生成器装饰器 也让人头疼不已。

为了语法更加简洁。于是,在Python3.5(PEP 492)中新增了 async/await 语法来实现异步协程。

async/await的使用

先介绍几个概念:

  • async/await :python3.5之后用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。
  • event_loop :事件循环,程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。
  • coroutine :协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
  • task :任务,是对协程进一步封装,其中包含任务的各种状态。
  • future : 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别

1.创建协程

在def前加上async的声明,就完成了一个协程函数的定义。 协程函数不能直接调用运行,需要将协程注册到事件循环,并启动事件循环才能使用

import asyncio

async def fun(a):    # 定义协程函数
    print(a)

# 调用协程函数,生成一个协程对象,此时协程函数并未执行
coroutine = fun('hello world')
# 创建事件循环
loop = asyncio.get_event_loop()
# 将协程函数添加到事件循环,并启动
loop.run_until_complete(coroutine)

# 输出
hello word

2. 任务对象task

协程对象不能直接运行,在注册事件循环的时候,其实是run_until_complete方法将协程包装成为了一个任务(task)对象。我们也可以显式实现它。

实现方式1:

使用create_task()创建task。

import asyncio

async def fun(a):
    print(a)
    return a

coroutine = fun('hello world')
loop = asyncio.get_event_loop()
# 使用create_task()创建task,并将coroutine对象转化成task对象
task = loop.create_task(coroutine)
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')
print(f'result: {result}')

输出结果:

task: <Task pending coro=<fun() running at D:/test.py:3>>
hello world
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>

从输出结果能看到,创建task对象后,未将task添加到事件循环之前,状态是 pending ;task对象执行完毕后,状态是 finished ,并将参数 a 的值返回。

实现方式2:

使用asyncio 的 ensure_future() 方法,创建task。

import asyncio

async def fun(a):
    print(a)
    return a

coroutine = fun('hello world')
# 使用asyncio 的 ensure_future() 方法,创建task,并将coroutine对象转化成task对象
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')

输出结果:

task: <Task pending coro=<fun() running at D:/test.py:3>>
hello world
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>

通过 ensure_future() 可以在 loop 未定义前创建task。实现效果与上面相同。

3.绑定回调函数

如果需要在task执行完毕后对结果进行处理,可以通过给task绑定回调函数完成,回调的最后一个参数是future对象(如task对象)。

import asyncio

async def fun(a):
    print(a)
    return a

def callback(task): # 回调函数,打印task的返回值
    print(f'result: {task.result()}')

coroutine = fun('hello world')
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
task.add_done_callback(callback)    #绑定回调函数
print(f'task: {task}')
loop.run_until_complete(task)
print(f'task: {task}')

输出结果:

task: <Task pending coro=<fun() running at D:/test.py:3> cb=[callback() at D:/Study/Python/python_text/非项目/协程.py:7]>
hello world
result: hello world # 完成了返回值的打印
task: <Task finished coro=<fun() done, defined at D:/test.py:3> result='hello world'>

4.多任务协程

如果我们需要执行多个任务时,我们可以定义一个任务列表,并将需要完成的协程任务都加进去。将原本的 loop.run_until_complete(tasks) 改为 loop.run_until_complete(asyncio.wait(tasks))

如果执行的是多个 耗时 的任务,如网络请求、文件读取等。此时就 await 就派上用场了, await 可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。 协程遇到await,事件循环将会挂起该协程,执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行

举个例子:

import time
import asyncio

async def taskIO_1():   
    print('开始运行IO任务1...')
    await asyncio.sleep(2)  
    print('IO任务1已完成,耗时2s')
    return taskIO_1.__name__

async def taskIO_2():       
    print('开始运行IO任务2...')
    await asyncio.sleep(3)  
    print('IO任务2已完成,耗时3s')
    return taskIO_2.__name__

if __name__ == '__main__':
    start = time.time()
    loop = asyncio.get_event_loop() 
    tasks = [taskIO_1(), taskIO_2()]
    loop.run_until_complete(asyncio.wait(tasks)) # 完成事件循环,直到最后一个任务结束
    print('所有IO任务总耗时%.5f秒' % float(time.time()-start))
    
# 输出
开始运行IO任务2...
开始运行IO任务1...
IO任务1已完成,耗时2s
IO任务2已完成,耗时3s
所有IO任务总耗时3.00251秒

可以看出,原本需要5秒,现在执行只需要3秒。

yield from转async/await

上述代码有没有很眼熟。

其实,这段代码正是 【yield from实现异步协程】 末尾, yield from 结合 asynico 实现异步协程的代码。只是将yielf from风格变为async/await风格。

由于 async/awaityield from 风格的协程底层实现方式相同。因此,从 yield from 风格改为 async/await 风格非常容易。只需:

  • @asyncio.coroutine 替换为 async
  • yield from 替换为 await

async/await 风格的代码隐藏了装饰器、 yield from 语法,方便了人们的理解,同时也让代码更加简洁。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK