asyncio中await关键字如何使用

本文深入探讨了Python中的await关键字及其在协程中的作用,阐述了await如何实现阻塞和获取协程返回值。通过示例程序展示了如何在并发任务中使用await,以及create_task()和asyncio.sleep()的结合使用来控制并发执行。同时,分析了在并发任务中await的位置对程序执行效果的影响,强调了await在确保并发任务执行完成和接收返回值上的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. await的作用

async def main():
    ...                    # ①
    result = await xxx()   # ②
    ...                    # ③
    result2 = await yyy()  # ④

await类似yield from,程序main()执行到await表达式处(②),main()会在②处阻塞,③④所在程序无法继续执行下去,直到xxx()执行完并返回结果。

xxx()执行完并返回结果后,返回结果赋值给变量result,然后main()继续执行下去。

await的作用就是阻塞调用方,并获取await xxx()中xxx的返回值。

await好像就是一种控制流程,下面if语句进行不严谨类比。

def main():
    ...
    _r = None
    if True:        # ①
        _r = xxx()  # ②
    result = _r     # ③
    ...             # ④

程序执行到①处,就会在if分支里面(②)执行,③处的程序只在执行②执行完了,才能继续。

await就类似于if True了,await xxx()中的xxx()就类似if语句里面逻辑了。

2. 如何在协程中使用await?

# 程序一

import time
import asyncio
from collections import namedtuple

Result = namedtuple('Result', 'name, result')

async def factorial(name, n):
    f = 1
    for i in range(1, n + 1):
        f *= i
    await asyncio.sleep(n)
    print(f'> {time.strftime("%X")} - ({name}) Factorial({n})={f}')
    return Result(name, f)

async def main():
    print(f'=== start time: {time.strftime("%X")}')
    r = await factorial('fact-1', 3)
    print(f'Second time:{time.strftime("%X")}, Result: <{r.name}, {r.result}>')

    r = await factorial('fact-2', 5)
    print(f'=== Third time:{time.strftime("%X")}, Result: <{r.name}, {r.result}>')

# if __name__ == '__main__':  在非Jupyter上应该使用这个
#     asyncio.run(main())
await main()  # 在Jupyter上执行

执行结果:

=== start time: 22:57:35
> 22:57:38 - (fact-1) Factorial(3)=6
Second time:22:57:38, Result: <fact-1, 6>
> 22:57:43 - (fact-2) Factorial(5)=120
=== Third time:22:57:43, Result: <fact-2, 120>
# 程序二

async def main():
    print(f'=== start time: {time.strftime("%X")}')
    r = await asyncio.create_task(factorial('fact-1', 3))
    print(f'Second time:{time.strftime("%X")}, Result: <{r.name}, {r.result}>')

    r = await asyncio.create_task(factorial('fact-2', 5))
    print(f'=== Third time:{time.strftime("%X")}, Result: <{r.name}, {r.result}>')

await main()

执行结果:

=== start time: 22:40:07
> 22:40:10 - (fact-1) Factorial(3)=6
Second time:22:40:10, Result: <fact-1, 6>
> 22:40:15 - (fact-2) Factorial(5)=120
=== Third time:22:40:15, Result: <fact-2, 120>

程序一和程序二的执行结果都说明了await 阻塞作用,只有执行完await表达式里的协程factorial()main()函数才能继续执行下面的代码。

上面两个程序,去掉asyncawait 关键字,执行的效果同平时写的同步代码是一样的。

所以说,如果不是为了并发,使用async/await的效果等同于用同步方式写的,还增加代码的理解难度。

程序二中,asyncio.create_task()方法是可以用来并发运行的。

3. await在运行并发任务上的如何使用

await关键字使用上的疑惑和纠结,就是我要并发,但await这个关键字我该怎么使用?在何处用?

想要并发,但await的作用却是阻塞程序,并发和await这两个就是矛盾体。没写好,就写成同步去了… 类似程序二,明明create_task()用来并发,执行结果却没效果。

await在何处使用关乎并发的效果。

# 程序三

async def main():
    print(f'=== start time: {time.strftime("%X")}')
    task1 = asyncio.create_task(factorial('fact-1', 3))  # ①
    task2 = asyncio.create_task(factorial('fact-2', 7))  # ②

    print('await start ...')  # ③

    r = await task2  # ④
    print(f'=== Second time:{time.strftime("%X")}, Result: <{r.name}, {r.result}>')

    r = await task1 # ⑤
    print(f'=== Third time:{time.strftime("%X")}, Result: <{r.name}, {r.result}>')

await main()

执行结果:

=== start time: 22:44:25
await start ...
> 22:44:28 - (fact-1) Factorial(3)=6
> 22:44:32 - (fact-2) Factorial(7)=5040
=== Second time:22:44:32, Result: <fact-2, 5040>
=== Third time:22:44:32, Result: <fact-1, 6>

程序三跟程序二不同点在于create_task()处没有添加await去阻塞。

create_task()会将协程封装成一个Task,并返回一个Task对象。该方法不会阻塞main()程序,所以,①②处的代码瞬间就执行完了,并继续执行下去。

create_task()创建的Task,会自动添加到事件循环上,通俗的说,事件循环就是调度Task并发的,但何时并发,我们是无法控制的。

所以,使用create_task()来并发是“隐性”的。这个隐性是指,我们没有类似多线程那样有很明确的start()启动并发。而是执行了create_task()后就算是启动了并发。

当程序执行到③时,①②创建的两个任务可能在并发执行了(如果没有,可能过一会儿就并发了)。

程序四的执行情况就说明了create_task()执行后在并发执行

现在疑惑,既然并发执行了,为什么需要在④和⑤上添加await?

用程序五来说明

# 程序四
async def main():
    """
    task是自行调度的,即使没有await task1也是在执行的。
    """
    print(f'\n=== start time: {time.strftime("%X")}')
    asyncio.create_task(factorial('fact-1', 3))
    asyncio.create_task(factorial('fact-2', 7))

    print('await start ...')
    await asyncio.sleep(20)
    print(f'=== End time: {time.strftime("%X")}')

await main()

执行结果:

=== start time: 22:44:43
await start ...
> 22:44:46 - (fact-1) Factorial(3)=6
> 22:44:50 - (fact-2) Factorial(7)=5040
=== End time: 22:45:03
# 程序五
async def main():
    """
    create_task()会返回一个task对象,task会被事件调度器自动处理,
    该方法不会阻塞,所以返回task对象后会执行执行。
    """
    print(f'=== start time: {time.strftime("%X")}')
    asyncio.create_task(factorial('fact-1', 3))  # ①
    asyncio.create_task(factorial('fact-2', 7))  # ②

    print('await start ...')
    await asyncio.sleep(6)  # ③
    print(f'=== End time: {time.strftime("%X")}')

await main()

执行结果:

=== start time: 22:45:03
await start ...
> 22:45:06 - (fact-1) Factorial(3)=6
=== End time: 22:45:09
> 22:45:10 - (fact-2) Factorial(7)=5040

①处是个耗时3s的操作,②是耗时7s的操作。③是用来模拟main()这个主线程执行完要6s。而当主线程执行完后就关闭了。

在主线程存活这6s内,开启的协程①是可以执行结束的,而协程②要7s才能执行完。故当主线程关闭了,里面还没有执行完的任务(这里是②)也一同被清了。类似电脑主机关机了,里面还在跑的程序也被强制关停了。

所以,在并发上,await是可以用来保证并发任务执行结束,同时接收并发任务返回值的,如程序三。

如果不关心是否要执行完,确实是可以不用await的。比如在一个死循环里或者不停服的代码里并发。

所以,await放在程序的位置是很关键的。如果要并发多个任务,写完多个create_task(),如果后面的代码跟并发返回结果无关联,最好将await放在其他代码后面。不然直接create_task()就await了,导致后面的代码无法执行,这样效果就不甚理想。

async main():
    ...
    task1 = asyncio.create_task(coro())   # ①
    task2 = asyncio.create_task(coro())   # ②
    ...
    ...   # 这里表示其他要执行的逻辑代码  ③
    r = await task1   # await不直接放在②后面,而放在③后面,这样③处的执行才不被阻塞。
    r2 = await task2

同理,在使用asyncio.as_completed()来开启并发,如何使用await,是跟使用create_task()思路一样的。

如果不需要返回值并且不关心是否正常执行完,可以不使用await,如果要用且在不影响其他逻辑代码情况,尽量对任务并发的结果放在后面处理。

但如果使用asyncio.gather()asyncio.wait()来并发操作,因为这两个方法语法原因,都是需要使用到await的。

所以要用asyncio.gather()asyncio.wait(),不用想,在它们前面加await就好。

### Python 中 `asyncio` 库及 `await` 关键字使用教程 #### 一、基础概念介绍 在 Python 的异步编程模型中,`asyncio` 是核心库之一。它提供了编写单线程并发代码所需的工具,基于协程、事件循环、任务和其他原语。 - **协程 (Coroutine)**:由带有 `async def` 声明的函数定义而成的对象。当调用这样的函数时,并不会立即执行其中的内容而是返回一个可以被挂起和恢复运行状态的特殊对象——即协程对象[^3]。 - **事件循环 (Event Loop)**:程序启动后进入的一个无限循环过程,在此期间不断监听并处理各种 I/O 或定时器触发的任务。所有的异步操作都依赖于这个中心化的调度机制来协调各个独立工作的单元之间的协作关系[^1]。 - **Task**:为了更好地管理和跟踪多个正在运行中的协程实例的状态变化情况而引入的概念;可以通过 `asyncio.create_task()` 方法显式地将某个普通的协程封装成具有更高优先级级别的 Task 实体以便更高效地参与竞争资源分配权衡考量之中。 - **Future**:表示尚未完成的工作的结果。它可以用来桥接同步世界与异步世界的鸿沟,允许我们以一种统一的方式处理来自不同源头的数据流或计算结果。不过这部分内容暂时还未完全展开说明。 #### 二、`await` 关键字详解 `await` 主要用于等待另一个可等待对象(如上述提到过的三种类型)结束其工作流程后再继续向下执行后续逻辑分支。需要注意的是只有处于异步上下文中才能合法地运用该语法结构: ```python import asyncio async def fetch_data(url): # 模拟网络请求,这里使用 asyncio.sleep 替代实际 IO 操作 await asyncio.sleep(1) # 此处会暂停当前协程直到睡眠时间到达为止 return f"Data from {url}" ``` 这段代码展示了最简单的形式下如何利用 `await` 来实现非阻塞式的延时效果模拟真实场景下的数据获取行为[^2]。 对于包含多条连续性的 `await` 表达式的场合,则遵循先进先出原则逐个解决每一个待办事项之后才会推进至下一步动作: ```python async def main(): data1 = await fetch_data("https://api.example.com/data1") # 首次遇到 await ,整个方法会被挂起直至左侧表达式求值完毕 print(data1) data2 = await fetch_data("https://api.example.com/data2") # 类似地再次遭遇第二个 await 节点... print(data2) if __name__ == "__main__": asyncio.run(main()) ``` 然而这种串行化的设计模式显然不是最优解方案,特别是在面对大量相似性质的小型子任务集合时效率低下难以满足高性能需求的应用环境要求。因此官方推荐采用批量提交策略并通过诸如 `asyncio.gather()` 函数一次性收集所有预期产出项从而达到提高吞吐量的目的: ```python async def parallel_fetches(): urls = ["https://api.example.com/data{}".format(i) for i in range(1, 4)] tasks = [fetch_data(url) for url in urls] results = await asyncio.gather(*tasks) # 并发执行多个异步操作并将最终得到的所有响应打包在一起返回给调用者 for result in results: print(result) if __name__ == "__main__": asyncio.run(parallel_fetches()) ``` 此外还有其他一些高级特性比如超时控制(`wait_for`)、条件判断(`as_completed`)等可以帮助开发者更加灵活自如地掌控复杂的业务逻辑流转路径。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值