Python协程(asyncio)(一)协程开发

Python实用教程_spiritx的博客-CSDN博客

协程的概念

Python因为GIL的原因,线程的实际并行效果在很多场景下并不明显,不能很好的发挥多核CPU的性能,使用进程可以很好的发挥CPU的性能,但是进程的开销大,并且进程之间的通讯实现起来比较复杂,为了更简便的开发并发的应用,Python引入了一种新的机制--协程。

协程是一种轻量级的线程,可以在一个线程中实现并发。与多线程和多进程不同,协程是由程序员自己控制的。在协程中,程序员可以手动挂起和恢复线程的执行。因此,协程可以更好地支持异步编程,以及在I/O密集型应用中更高效地利用计算机资源。

进程(process):进程是操作系统资源分配的最小单位。进程有自己的虚拟地址空间,这个空间包括了各种资源,例如堆、栈,各种段,它们其实都是虚拟地址空间的一块区域。所以说进程是资源分配的最小单位。
线程(thread):线程是操作系统任务调度和执行的最小单位。线程包含在进程之中,是进程中实际运作单位。
协程(coroutine):线程中协作式调度的程序(函数),协程是一种比线程更加轻量级的存在,一个线程可以拥有多个协程。


线程和协程区别
①线程是由操作系统调度和执行的。协程是完全由程序代码所控制。协程仅仅是一个特殊的函数,它可以在某个地方挂起,并且可以在挂起处继续执行,这样的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。一个线程内的多个协程虽然可以切换(切换者是开发者),但是多个协程是串行执行的,只能在一个线程内运行,没法利用CPU多核能力
②线程是操作系统抢占式调度的。协程是开发者自己决定什么时候让出,协程完全是用户态的行为,由程序员决定什么时候让出控制权,保存现场和切换恢复使用的资源也非常少,提高了处理效率  多进程和多线程除了创建的开销大之外还有一个难以根治的缺陷,就是处理进程之间或线程之间的协作问题,因为是依赖多进程和多线程的程序在不加锁的情况下通常是不可控的,而协程则可以完美地解决协作问题,由用户来决定协程之间的调度。

python实现协程的方法有很多,早期的有greenlet库、curio库等,也可以通过生成器yield,本文学习的是从3.4开始支持的asyncio库以及3.5开始支持的async和await关键字的实现方式。

协程是基于生成器yield开发的,核心是异步处理机制,所以3.5之后支持协程的模块为asyncio,实际是异步IO的模块。

异步模型

在学习asyncio之前,我们先来理清楚同步/异步的概念。

基本概念

阻塞

  • 程序未得到所需计算资源时被挂起的状态。
  • 程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。
  • 常见的阻塞形式有:网络I/O阻塞、磁盘I/O阻塞、用户输入阻塞等。

阻塞是无处不在的,包括CPU切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。(如果是多核CPU则正在执行上下文切换操作的核不可被利用。)

非阻塞

  • 程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。
  • 非阻塞并不是在任何程序级别、任何情况下都可以存在的。
  • 仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。

非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

同步

指完成事务的逻辑,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行。

异步

异步是和同步相对的,异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果,直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果。

例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。简言之,异步意味着无序。

异步模型

异步处理采用事件循环的模式,重复读取消息—处理消息的循环过程,也就是说异步模型需要一个事件循环,将处理函数注册到事件循环上,当事件发生时,触发处理函数的执行,执行完成后(或等待新的事件),将重新在事件循环上等待新的事件发生:

  • 事件循环:程序开启一个无限的循环,程序员会把一些处理函数(协程函数)注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。
  • 协程函数:是一种特别的函数,直接对它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用执行。

协程

在Python中,协程实际是一个异步函数,但与普通的函数在定义上有区别。

一般的函数是这样的:

def regular_double(x):
    return 2 * x

而一个异步函数需要使用async关键字来定义:

async def async_double(x):
    return 2 * x

为了区分这两类函数,前面的那个我们称呼为同步函数,后面的那个称呼为异步函数。
从外观上看异步函数和同步函数没什么区别,异步函数只是在前面多了个async。
“Async” 是“asynchronous”的简写,从用户角度异步函数和同步函数有以下区别:

  • 要调用异步函数,必须使用await关键字。 因此,调用异步函数时,不能直接写async_double(3),而是写await async_double(3).
  • 不能在同步函数里使用await,否则会出错。

句法错误:

def print_double(x):
    print(await async_double(x))   # <-- SyntaxError here

但是在异步函数中,await是被允许的:

async def print_double(x):
    print(await async_double(x))   # <-- OK!

如果要运行一个异步方法,需要使用特定的异步调用接口函数: 

import asyncio


async def foo(name:str):
    print(f'Hello {name}!')
    await asyncio.sleep(1)
    print(f'Bye {name}!')

asyncio.run(foo('World’))

‘’'
Hello World!
Bye World!
‘''

从上面的例子也可以看出,异步函数内是可以调用同步函数的。

前面介绍过,一个异步函数就是一个协程

协程通常表示两个紧密关联的概念:

  • 协程函数: 定义形式为 async def 的函数;
  • 协程对象: 调用 协程函数 所返回的对象。

注意:简单地调用一个协程并不会使其被调度执行

>>>import asyncio

>>>async def main():
    print('hello')
    await asyncio.sleep(1)
    print('world')

>>>asyncio.run(main())
hello
world
>>>main()
<coroutine object main at 0x1053bb7c8>
import asyncio
async def main():
    await asyncio.sleep(1)
    print('hello')

coro  = main()
print(type(coro))

‘’'
<class 'coroutine'>
sys:1: RuntimeWarning: coroutine 'main' was never awaited
‘''

可等待对象

如果一个对象可以在 await 语句中使用,那么它就是 可等待 对象。许多 asyncio API 都被设计为接受可等待对象。
可等待 对象有三种主要类型: 协程、 任务(Task) 和 Future。

可等待对象(awaitable object)通常实现一个 __await__() 特殊方法,它必须返回一个迭代器。协程对象(Coroutine objects)通过 async def 定义的函数返回的是可等待对象。
但是,生成器对象通过 types.coroutine() 或者 asyncio.coroutine() 返回的迭代器也是可等待对象, 但它们并没有实现 __await__()。

另外 await 语法则是被用来告知 Python 可以在此处暂停执行 coroutine 转而执行其他工作,而且该语法只能在 coroutine 数据内使用,因此 async def 与 await 通常会一起出现。

await 语法的另外重点是 await 之后只能接 awaitables 可等待对象,例如 coroutine 或者是之后会介绍到的 Task, Future 以及有实现 __await__() 方 的对象,所以不是所有的对象或操作都能够用 await 进行暂停。因此,我们在使用三方库时,要看它的相关方法是否实现了可等待特性。

运行协程

要实际运行一个协程,asyncio 提供了以下几种机制:

  • asyncio.run() 函数调用;
  • 使用await调用协程执行;
  • asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程。
  • asyncio.TaskGroup 类提供了 create_task() 的更现代化的替代.

下面分别学习这几种方式:

asyncio.run()

asyncio.run(coro, ***, debug=False)

执行协程 coro 并返回结果。

  • asyncio.run()函数内部管理了一个事件循环,该方法实际是将协程coro注册到内部管理的事件循环上,并立即启动事件循环。
  • 当有其他 asyncio 事件循环在同一线程中运行时,此函数不能被调用。
  • 如果 debug 为 True,事件循环将运行于调试模式。 False 将显式地禁用调试模式。 使用 None 将沿用全局 Debug 模式设置。
  • 此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。
async def main():
    await asyncio.sleep(1)
    print('hello')

asyncio.run(main())

asyncio.create_task()

asyncio.create_task(coro*name=Nonecontext=None)

将 coro 协程 封装为一个 Task 并调度其执行。返回 Task 对象。

  • name 不为 None,它将使用 Task.set_name() 来设为任务的名称。
  • 可选的 context 参数允许指定自定义的 contextvars.Context 供 coro 运行。 当未提供 context 时将创建当前上下文的副本。
  • 该任务会在内部的一个事件循环中执行。
import asyncio

async def nested():
    return 42

async def main():
    # Schedule nested() to run soon concurrently
    # with "main()".
    task = asyncio.create_task(nested())

    # "task" can now be used to cancel "nested()", or
    # can simply be awaited to wait until it is complete:
    await task

asyncio.run(main())

保存一个指向此函数的结果的引用,以避免任务在执行过程中消失。 事件循环将只保留对任务的弱引用。 未在其他地方被引用的任务可能在任何时候被作为垃圾回收,即使是在它被完成之前。 如果需要可靠的“发射后不用管”后台任务,请将它们放到一个多项集中:

background_tasks = set()

for i in range(10):
    task = asyncio.create_task(some_coro(param=i))

    # Add task to the set. This creates a strong reference.
    background_tasks.add(task)

    # To prevent keeping references to finished tasks forever,
    # make each task remove its own reference from the set after
    # completion:
    task.add_done_callback(background_tasks.discard)

注意:如果启动多个任务执行,它们是“并行”执行的: 

import asyncio
import time

async def say_after(delay, what):
    print(f"Begin {what}, at {time.strftime('%X')}")
    await asyncio.sleep(delay)
    print(f"End {what}, at {time.strftime('%X')}")

async def main():
    task1 = asyncio.create_task(say_after(3, 'First'))
    task2 = asyncio.create_task(say_after(2, 'Second'))
    print(f"main Begin at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"main finished at {time.strftime('%X')}")

asyncio.run(main())

‘’’
main Begin at 22:24:14
Begin First, at 22:24:14
Begin Second, at 22:24:14
End Second, at 22:24:16
End First, at 22:24:17
main finished at 22:24:17
’‘’

First和Second几乎是同时启动的,Second在后,但是因为等待的时间比较短,是先完成的。

Task对象

class asyncio.Task(coro*loop=Nonename=Nonecontext=None)

asyncio.create_task()生成的任务对象。

Task 对象被用来在事件循环中运行协程。事件循环使用协同日程调度: 一个事件循环每次运行一个 Task 对象。

主要的方法和属性有:

方法和属性名说明
done()如果Task对象已经执行完成,则返回 True。当 Task 所封包的协程返回一个值、引发一个异常或 Task 本身被取消时,则会被认为 已完成
result()返回 Task 的结果。
如果 Task 对象 已完成,其封包的协程的结果会被返回 (或者当协程引发异常时,该异常会被重新引发。)
如果 Task 对象 被取消,此方法会引发一个 CancelledError 异常。
如果 Task 对象的结果还不可用,此方法会引发一个 InvalidStateError 异常。
exception()返回 Task 对象的异常。
如果所封包的协程引发了一个异常,该异常将被返回。如果所封包的协程正常返回则该方法将返回 None。
如果 Task 对象 被取消,此方法会引发一个 CancelledError 异常。
如果 Task 对象尚未 完成,此方法将引发一个 InvalidStateError 异常。
add_done_callback(callback*context=None)添加一个回调,将在 Task 对象 完成 时被运行。
此方法应该仅在低层级的基于回调的代码中使用。
remove_done_callback(callback)从回调列表中移除 callback 。
get_stack(*limit=None)返回此 Task 对象的栈框架列表。
如果所封包的协程未完成,这将返回其挂起所在的栈。如果协程已成功完成或被取消,这将返回一个空列表。如果协程被一个异常终止,这将返回回溯框架列表。
框架总是从按从旧到新排序。
每个被挂起的协程只返回一个栈框架。
可选的 limit 参数指定返回框架的数量上限;默认返回所有框架。返回列表的顺序要看是返回一个栈还是一个回溯:栈返回最新的框架,回溯返回最旧的框架。(这与 traceback 模块的行为保持一致。)
print_stack(*limit=Nonefile=None)打印此 Task 对象的栈或回溯。
此方法产生的输出类似于 traceback 模块通过 get_stack() 所获取的框架。
limit 参数会直接传递给 get_stack()。
file 参数是输出所写入的 I/O 流;在默认情况下输出会写入到 sys.stdout。
get_coro()返回由 Task 包装的协程对象。
get_name()返回 Task 的名称。
set_name(value)设置 Task 的名称。
value 参数可以为任意对象,它随后会被转换为字符串。
在默认的 Task 实现中,名称将在任务对象的 repr() 输出中可见。
cancel(msg=None)请求取消 Task 对象。这将安排在下一轮事件循环中抛出一个 CancelledError 异常给被封包的协程。
cancelled()如果 Task 对象 被取消 则返回 True。当使用 cancel() 发出取消请求时 Task 会被 取消,其封包的协程将传播被抛入的 CancelledError 异常。
uncancel()递减对此任务的取消请求计数。
返回剩余的取消请求数量。
请注意被取消的任务执行完成,继续调用 uncancel() 将是低效的。
cancelling()返回对此 Task 的挂起请求次数,即对 cancel() 的调用次数减去 uncancel() 的调用次数。
请注意如果该数字大于零但相应 Task 仍在执行,cancelled() 仍将返回 False。 这是因此该数字可通过调用 uncancel() 来减少,这会导致任务在取消请求降到零时尚未被取消。
async def cancel_me():
    print('cancel_me(): before sleep')

    try:
        # Wait for 1 hour
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        print('cancel_me(): cancel sleep')
        raise
    finally:
        print('cancel_me(): after sleep')

async def main():
    # Create a "cancel_me" Task
    task = asyncio.create_task(cancel_me())

    # Wait for 1 second
    await asyncio.sleep(1)

    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("main(): cancel_me is cancelled now")

asyncio.run(main())

# Expected output:
#
#     cancel_me(): before sleep
#     cancel_me(): cancel sleep
#     cancel_me(): after sleep
#     main(): cancel_me is cancelled now

class asyncio.TaskGroup

持有一个任务分组的 异步上下文管理器。 可以使用 create_task() 将任务添加到分组中。 当该上下文管理器退出时所有任务都将被等待。

异步上下文管理器

异步上下文管理器 是 上下文管理器 的一种,它能够在其 __aenter__ 和 __aexit__ 方法中暂停执行。
异步上下文管理器可在 async with 语句中使用。

object.__aenter__(self)
在语义上类似于 __enter__(),仅有的区别是它必须返回一个 可等待对象。

object.__aexit__(self, exc_type, exc_value, traceback)
在语义上类似于 __exit__(),仅有的区别是它必须返回一个 可等待对象。

class AsyncContextManager:
    async def __aenter__(self):
        await log('entering context')

    async def __aexit__(self, exc_type, exc, tb):
        await log('exiting context')

使用例子

TaskGroup对象的核心方法是create_task(),它的签名和参数都与asyncio.create_task()一样,使用方法也一样:

async def main():
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(some_coro(...))
        task2 = tg.create_task(another_coro(...))
    print("Both tasks have completed now.")

async with 语句将等待tg组中的所有任务结束。 在等待期间,仍可将新任务添加到tg组中 (例如,通过将 tg 传入某个协程并在该协程中调用 tg.create_task())。 一旦最后的任务完成并退出 async with 代码块,将无法再向分组添加新任务。

当首次有任何属于tg组的任务因 asyncio.CancelledError 以外的异常而失败时,tg组中的剩余任务将被取消。(类似于事务的一致性,一旦某个失败了,其他的也全部都取消执行) 。在此之后将无法添加更多任务到该分组中。 在这种情况下,如果 async with 语句体仍然为激活状态(即 __aexit__() 尚未被调用),则直接包含 async with 语句的任务也会被取消。 结果 asyncio.CancelledError 将中断一个 await,但它将不会跳出包含的 async with 语句。

一旦所有任务被完成,如果有任何任务因 asyncio.CancelledError 以外的异常而失败,这些异常会被组合在 ExceptionGroup 或 BaseExceptionGroup 中并将随后引发。

两个基础异常会被特别对待:如果有任何任务因 KeyboardInterrupt 或 SystemExit 而失败,任务分组仍然会取消剩余的任务并等待它们,但随后初始 KeyboardInterrupt 或 SystemExit 而不是 ExceptionGroup 或 BaseExceptionGroup 会被重新引发。

如果 async with 语句体因异常而退出(这样将调用 __aexit__() 并附带一个异常),此种情况会与有任务失败时一样对待:剩余任务将被取消然后被等待,而非取消类异常会被加入到一个异常分组并被引发。 传入到 __aexit__() 的异常,除了 asyncio.CancelledError 以外,也都会被包括在该异常分组中。 同样的特殊对待也适用于上一段所说的 KeyboardInterrupt 和 SystemExit。

与create_task()一样,启动多个任务时,它们都是“并行”执行的:

import asyncio
import time

async def say_after(delay, what):
    print(f"Begin {what}, at {time.strftime('%X')}")
    await asyncio.sleep(delay)
    print(f"End {what}, at {time.strftime('%X')}")

async def main():
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(
            say_after(3, 'First'))

        task2 = tg.create_task(
            say_after(1, 'Second'))

    print(f"main Begin at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"main finished at {time.strftime('%X')}")

asyncio.run(main())

‘’'
Begin First, at 22:28:41
Begin Second, at 22:28:41
End Second, at 22:28:42
End First, at 22:28:44
main Begin at 22:28:44
main finished at 22:28:44
‘''

并发运行任务

awaitable asyncio.gather(*awsreturn_exceptions=False)

并发 运行 aws 序列中的 可等待对象。
如果 aws 中的某个可等待对象为协程,它将自动被作为一个任务调度。
如果所有可等待对象都成功完成,结果将是一个由所有返回值聚合而成的列表。结果值的顺序与 aws 中可等待对象的顺序一致。
如果 return_exceptions 为 False (默认),所引发的首个异常会立即传播给等待 gather() 的任务。aws 序列中的其他可等待对象 不会被取消 并将继续运行。
如果 return_exceptions 为 True,异常会和成功的结果一样处理,并聚合至结果列表。
如果 gather() 被取消,所有被提交 (尚未完成) 的可等待对象也会 被取消。
如果 aws 序列中的任一 Task 或 Future 对象 被取消,它将被当作引发了 CancelledError 一样处理 -- 在此情况下 gather() 调用 不会 被取消。这是为了防止一个已提交的 Task/Future 被取消导致其他 Tasks/Future 也被取消。

一种更现代化的创建和并发运行任务然后等待它们完成的方式是 asyncio.TaskGroup。

import asyncio

async def factorial(name, number):
    f = 1
    for i in range(2, number + 1):
        print(f"Task {name}: Compute factorial({number}), currently i={i}...")
        await asyncio.sleep(1)
        f *= i
    print(f"Task {name}: factorial({number}) = {f}")
    return f

async def main():
    # Schedule three calls *concurrently*:
    L = await asyncio.gather(
        factorial("A", 2),
        factorial("B", 3),
        factorial("C", 4),
    )
    print(L)

asyncio.run(main())

# Expected output:
#
#     Task A: Compute factorial(2), currently i=2...
#     Task B: Compute factorial(3), currently i=2...
#     Task C: Compute factorial(4), currently i=2...
#     Task A: factorial(2) = 2
#     Task B: Compute factorial(3), currently i=3...
#     Task C: Compute factorial(4), currently i=3...
#     Task B: factorial(3) = 6
#     Task C: Compute factorial(4), currently i=4...
#     Task C: factorial(4) = 24
#     [2, 6, 24]

如果 return_exceptions 为 False,则在 gather() 被标记为已完成后取消它将不会取消任何已提交的可等待对象。 例如,在将一个异常传播给调用者之后,gather 可被标记为已完成,因此,在从 gather 捕获一个(由可等待对象所引发的)异常之后调用 gather.cancel() 将不会取消任何其他可等待对象。

asyncio.sleep()

coroutine asyncio.sleep(delayresult=None)

阻塞 delay 指定的秒数。如果指定了 result,则当协程完成时将其返回给调用者。sleep() 总是会挂起当前任务,以允许其他任务运行。将 delay 设为 0 将提供一个经优化的路径以允许其他任务运行。 这可供长期间运行的函数使用以避免在函数调用的全过程中阻塞事件循环。以下协程示例运行 5 秒,每秒显示一次当前日期:

import asyncio
import datetime

async def display_date():
    loop = asyncio.get_running_loop()
    end_time = loop.time() + 5.0
    while True:
        print(datetime.datetime.now())
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(1)

asyncio.run(display_date())

屏蔽取消操作

awaitable asyncio.shield(aw)

保护一个 可等待对象 防止其被 取消。
如果 aw 是一个协程,它将自动被作为任务调度。

task = asyncio.create_task(something())
res = await shield(task)

相当于:

res = await something()

不同之处 在于如果包含它的协程被取消,在 something() 中运行的任务不会被取消。从 something() 的角度看来,取消操作并没有发生。然而其调用者已被取消,因此 "await" 表达式仍然会引发 CancelledError。
如果通过其他方式取消 something() (例如在其内部操作) 则 shield() 也会取消。
如果希望完全忽略取消操作 (不推荐) 则 shield() 函数需要配合一个 try/except 代码段,如下所示:

task = asyncio.create_task(something())
try:
    res = await shield(task)
except CancelledError:
    res = None

保存一个传给此函数的任务的引用,以避免任务在执行过程中消失。 事件循环将只保留对任务的弱引用。 未在其他地方被引用的任务可能在任何时候被作为垃圾回收,即使是在它被完成之前。

超时

asyncio.timeout(delay)

返回一个可被用于限制等待某个操作所耗费时间的 异步上下文管理器。
delay 可以为 None,或是一个表示等待秒数的浮点数/整数。 如果 delay 为 None,将不会应用时间限制;如果当创建上下文管理器时无法确定延时则此设置将很适用。
在两种情况下,该上下文管理器都可以在创建之后使用 Timeout.reschedule() 来重新安排计划。

async def main():
    async with asyncio.timeout(10):
        await long_running_task()

如果 long_running_task 耗费 10 秒以上完成,该上下文管理器将取消当前任务并在内部处理所引发的 asyncio.CancelledError,将其转化为可被捕获和处理的 TimeoutError。

asyncio.timeout() 上下文管理器负责将 asyncio.CancelledError 转化为 TimeoutError,这意味着 TimeoutError 只能在该上下文管理器 之外 被捕获。

捕获 TimeoutError 的示例:

async def main():
    try:
        async with asyncio.timeout(10):
            await long_running_task()
    except TimeoutError:
        print("The long operation timed out, but we've handled it.")

    print("This statement will run regardless.")

asyncio.timeout() 所产生的上下文管理器(class asyncio.Timeout)可以被重新调整到不同的终止点并执行检查。

class asyncio.Timeout(when)

一个用于撤销已过期协程的 异步内容管理器。
when 应当是一个指明上下文将要过期的绝对时间,由事件循环的时钟来计时。

  • 如果 when 为 None,则超时将永远不会被触发。
  • 如果 when < loop.time(),则超时将在事件循环的下一次迭代中被触发。

when() → float | None
返回当前终止点,或者如果未设置当前终止点则返回 None。

reschedule(when: float | None)
重新安排超时。

expired() → bool
返回上下文管理器是否已超出时限(过期)。

async def main():
    try:
        # We do not know the timeout when starting, so we pass ``None``.
        async with asyncio.timeout(None) as cm:
            # We know the timeout now, so we reschedule it.
            new_deadline = get_running_loop().time() + 10
            cm.reschedule(new_deadline)

            await long_running_task()
    except TimeoutError:
        pass

    if cm.expired():
        print("Looks like we haven't finished on time.")

asyncio.timeout_at(when)

类似于 asyncio.timeout(),不同之处在于 when 是停止等待的绝对时间,或者为 None。

async def main():
    loop = get_running_loop()
    deadline = loop.time() + 20
    try:
        async with asyncio.timeout_at(deadline):
            await long_running_task()
    except TimeoutError:
        print("The long operation timed out, but we've handled it.")

    print("This statement will run regardless.")

等待

coroutine asyncio.wait_for(awtimeout)

等待 aw 可等待对象 完成,指定 timeout 秒数后超时。
如果 aw 是一个协程,它将自动被作为任务调度。
timeout 可以为 None,也可以为 float 或 int 型数值表示的等待秒数。如果 timeout 为 None,则等待直到完成。
如果发生超时,将取消任务并引发 TimeoutError。
要避免任务 取消,可以加上 shield()。
此函数将等待直到 Future 确实被取消,所以总等待时间可能超过 timeout。 如果在取消期间发生了异常,异常将会被传播。
如果等待被取消,则 aw 指定的对象也会被取消。

async def eternity():
    # Sleep for one hour
    await asyncio.sleep(3600)
    print('yay!')

async def main():
    # Wait for at most 1 second
    try:
        await asyncio.wait_for(eternity(), timeout=1.0)
    except TimeoutError:
        print('timeout!')

asyncio.run(main())

# Expected output:
#
#     timeout!

coroutine asyncio.wait(aws*timeout=Nonereturn_when=ALL_COMPLETED)

并发地运行 aws 可迭代对象中的 Future 和 Task 实例并进入阻塞状态直到满足 return_when 所指定的条件。
可迭代对象 aws 不能为空并且不接受产生任务的生成器。
返回两个 Task/Future 集合: (done, pending)。

done, pending = await asyncio.wait(aws)

如指定 timeout (float 或 int 类型) 则它将被用于控制返回之前等待的最长秒数。
请注意此函数不会引发 TimeoutError。 当超时发生时尚未完成的 Future 或 Task 会在设定的秒数后被直接返回。

return_when 指定此函数应在何时返回。它必须为以下常数之一:

常量

描述

FIRST_COMPLETED

函数将在任意可等待对象结束或取消时返回。

FIRST_EXCEPTION

函数将在任意可等待对象因引发异常而结束时返回。当没有引发任何异常时它就相当于 ALL_COMPLETED

ALL_COMPLETED

函数将在所有可等待对象结束或取消时返回。

与 wait_for() 不同,wait() 在超时发生时不会取消可等待对象。

asyncio.as_completed()

asyncio.as_completed(aws*timeout=None)

并发地运行可迭代对象 aws 中的 可等待对象。 产生任务的生成器不可被用作 aws 可迭代对象。 返回一个产生协程的迭代器。 所返回的每个协程可被等待以便从剩余的可等待对象的可迭代对象中获得最早的下一个结果。
如果在所有 Future 对象完成之前发生超时则将引发 TimeoutError。

import asyncio
import time

async def say_after(delay, what):
    print(f"Begin {what}, at {time.strftime('%X')}")
    await asyncio.sleep(delay)
    print(f"End {what}, at {time.strftime('%X')}")
    return what

async def main():
    print(f"main Begin at {time.strftime('%X')}")

    aws = [asyncio.create_task(say_after(3, 'First')),
           asyncio.create_task(say_after(2, 'Second')),
           asyncio.create_task(say_after(1, 'Third'))]

    for coro in asyncio.as_completed(aws):
        earliest_result = await coro
        print(f'as_completed:', earliest_result)

    print(f"main finished at {time.strftime('%X')}")

asyncio.run(main())

‘’'
main Begin at 23:05:35
Begin First, at 23:05:35
Begin Second, at 23:05:35
Begin Third, at 23:05:35
End Third, at 23:05:36
as_completed: Third
End Second, at 23:05:37
as_completed: Second
End First, at 23:05:38
as_completed: First
main finished at 23:05:38
‘''

将普通函数转化为协程

coroutine asyncio.to_thread(func/*args**kwargs)

向此函数提供的任何 *args 和 **kwargs 会被直接传给 func。 并且,当前 contextvars.Context 会被传播,允许在不同的线程中访问来自事件循环的上下文变量。
返回一个可被等待以获取 func 的最终结果的协程。
这个协程函数主要是用于执行在其他情况下会阻塞事件循环的 IO 密集型函数/方法。 例如:

def blocking_io():
    print(f"start blocking_io at {time.strftime('%X')}")
    # Note that time.sleep() can be replaced with any blocking
    # IO-bound operation, such as file operations.
    time.sleep(1)
    print(f"blocking_io complete at {time.strftime('%X')}")

async def main():
    print(f"started main at {time.strftime('%X')}")

    await asyncio.gather(
        asyncio.to_thread(blocking_io),
        asyncio.sleep(1))

    print(f"finished main at {time.strftime('%X')}")


asyncio.run(main())

# Expected output:
#
# started main at 19:50:53
# start blocking_io at 19:50:53
# blocking_io complete at 19:50:54
# finished main at 19:50:54

在任何协程中直接调用 blocking_io() 将会在调用期间阻塞事件循环,导致额外的 1 秒运行时间。 但是,通过改用 asyncio.to_thread(),我们可以在单独的线程中运行它从而不会阻塞事件循环。

异步迭代器

Python 中的 iterator 指的是有实现 __iter__() 与 __next__() 两个方法的对象,因此 iterator 可以用 for 语句迭代。而 async for 则是为了 async 版的 iterator 而新增的语法, async 版的 iterator 则是需要实现 __aiter__() 与 __anext()__ 两个方法。

以下是实作 async iterator 的范例,可以看到 __anext__() 已经被转为 协程coroutine:

import asyncio

class AsyncCounter(object):

    def __init__(self, stop=None):
        self.count = 0
        self.stop = stop

    def __aiter__(self):
        return self

    async def __anext__(self):
        await asyncio.sleep(1)
        self.count += 1
        if self.stop == self.count:
            raise StopAsyncIteration
        return self.count

async def main():
    async for i in AsyncCounter(11):
        print(i)

asyncio.run(main())
'''
1
2
3
4
5
6
7
8
9
10
'''

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值