异步编程 asynico、async、await最佳实践

Asynico 最佳实践

术语:

  1. 协程(Coroutine):协程是一种支持异步操作和暂停执行的函数或代码块。使用 async 关键字定义的函数即为协程。
  2. 事件循环(Event Loop):事件循环是 asyncio 的核心组件,用于调度和执行协程任务。事件循环从任务队列中依次获取任务并执行,同时也负责处理 I/O 事件和定时事件。
  3. 任务(Task):任务是协程的包装,表示一个可被事件循环调度的异步操作。创建任务的方式是使用 asyncio.create_task() 函数。
  4. Future:Future 表示一个异步操作的结果。可以通过 asyncio.Future() 创建一个 Future 对象,并使用 set_result()set_exception() 方法设置 Future 的结果或异常。
  5. 异步上下文管理器(Async Context Manager):异步上下文管理器是一个可用于异步上下文的对象,类似于常规的上下文管理器。异步上下文管理器需要实现 __aenter__()__aexit__() 方法,以提供在进入和退出异步上下文时进行操作。
  6. 异步迭代器(Async Iterator):异步迭代器提供了可异步遍历的功能。异步迭代器需要实现 __aiter__()__anext__() 方法。
  7. 回调(Callback):回调函数是在某个操作完成后被调用的函数。在 asyncio 中,可以使用回调函数来处理异步操作完成后的结果或异常。
  8. 阻塞和非阻塞:阻塞指的是在进行某个操作时,如果无法取得需要的结果,那么程序会一直等待,直到结果可用。非阻塞指的是在进行某个操作时,如果无法取得需要的结果,程序会立即返回,而不会等待结果可用。
  9. 并发(Concurrency)和并行(Parallelism):并发指的是多个任务同时进行,但不一定是同时执行的;并行指的是多个任务真正的同时执行。asyncio 提供了并发的支持,但并不提供直接的并行处理。
  10. 线程安全(Thread-Safe):线程安全指的是多线程环境下,对共享资源进行操作时不会出现不确定的结果或数据损坏。

协程与任务

协程

coroutine -- 协程

通过 async/await 语法来声明 协程 是编写 asyncio 应用的推荐方式。 例如,以下代码段会打印 "hello",等待 1 秒,再打印 "world":

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

>>> asyncio.run(main())
hello
world

asyncio.run() 函数用来运行最高层级的入口点 "main()" 函数

实现简单的协程并发 运行两个 say_after 协程:

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)


async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

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

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1  #任务会被自动调度
    await task2 #任务会被自动调度

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

asyncio.run(main())

因为是并发的运行 可能会快1 秒左右

任务

在 asyncio 中,任务(Task)是一个协程对象的高级表示形式,可以通过 asyncio.create_task() 或 asyncio.ensure_future() 来创建任务。任务会被自动调度,可以由事件循环(Event Loop)来调用和并发执行。

下面是一个简单的示例,展示了如何创建和调用一个任务:

import asyncio

async def hello():
    print("Hello")
    await asyncio.sleep(1)
    print("World")

async def main():
    task = asyncio.create_task(hello())  # 创建任务
    await task  # 等待任务完成

asyncio.run(main())  # 运行主协程

可等待对象

如果一个对象可以在 await 语句中使用,那么它就是 可等待 对象。许多 asyncio API 都被设计为接受可等待对象。

可等待 对象有三种主要类型: 协程, 任务Future.

协程

Python 协程属于 可等待 对象,因此可以在其他协程中被等待:

import asyncio

async def nested():
    return 42

async def main():
    # Nothing happens if we just call "nested()".
    # A coroutine object is created but not awaited,
    # so it *won't run at all*.
    nested()

    # Let's do it differently now and await it:
    print(await nested())  # will print "42".

asyncio.run(main())

重要

在本文档中 "协程" 可用来表示两个紧密关联的概念:

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

任务

任务 被用来“并行的”调度协程

当一个协程通过 asyncio.create_task() 等函数被封装为一个 任务,该协程会被自动调度执行:

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())

Futures

Future 是一种特殊的 低层级 可等待对象,表示一个异步操作的 最终结果

当一个 Future 对象 被等待,这意味着协程将保持等待直到该 Future 对象在其他地方操作完毕。

在 asyncio 中需要 Future 对象以便允许通过 async/await 使用基于回调的代码。

通常情况下 没有必要 在应用层级的代码中创建 Future 对象。

Future 对象有时会由库和某些 asyncio API 暴露给用户,用作可等待对象:

async def main():
    await function_that_returns_a_future_object()

    # this is also valid:
    await asyncio.gather(
        function_that_returns_a_future_object(),
        some_python_coroutine()
    )

一个很好的返回对象的低层级函数的示例是 loop.run_in_executor()

创建任务

asyncio.create_task(coro, ***, name=None, context=None)

coro 协程 封装为一个 Task 并调度其执行。返回 Task 对象。name 不为 None,它将使用 Task.set_name() 来设为任务的名称。

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)

休眠

coroutine asyncio.sleep(delay, result=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())

asyncio.get_running_loop() 获取当前的协程循环

并发运行任务

awaitable asyncio.gather(**aws*, *return_exceptions=False)

并发 运行 aws 序列中的 可等待对象

如果 aws 中的某个可等待对象为协程,它将自动被作为一个任务调度。

如果所有可等待对象都成功完成,结果将是一个由所有返回值聚合而成的列表。结果值的顺序与 aws 中可等待对象的顺序一致。

如果 return_exceptionsFalse (默认),所引发的首个异常会立即传播给等待 gather() 的任务。aws 序列中的其他可等待对象 不会被取消 并将继续运行。

如果 return_exceptionsTrue,异常会和成功的结果一样处理,并聚合至结果列表。

如果 gather() 被取消,所有被提交 (尚未完成) 的可等待对象也会 被取消

如果 aws 序列中的任一 Task 或 Future 对象 被取消,它将被当作引发了 CancelledError 一样处理 -- 在此情况下 gather() 调用 不会 被取消。这是为了防止一个已提交的 Task/Future 被取消导致其他 Tasks/Future 也被取消。

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]

屏蔽取消操作

  • awaitable asyncio.shield(aw)

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

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

    相当于:res = await something()

超时

coroutine asyncio.timeout(delay)

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

其他

使用异步函数:Asynico是为了处理异步操作而设计的,因此使用异步函数而不是同步函数是最佳实践之一。使用async关键字将函数定义为异步函数,并使用await关键字来等待异步操作的结果。
示例:

import asyncio

async def my_async_function():
    # 异步操作
    await asyncio.sleep(1)
    return 'Done'

asyncio.run(my_async_function())

使用事件循环(Event Loop):Asynico通过事件循环来调度和协调异步操作。使用asyncio.get_event_loop()来获取默认的事件循环,然后使用loop.run_until_complete()来运行异步函数。
示例:

import asyncio

async def my_async_function():
    # 异步操作
    await asyncio.sleep(1)
    return 'Done'

loop = asyncio.get_event_loop()
result = loop.run_until_complete(my_async_function())
print(result)

并发执行多个异步任务:Asynico提供了asyncio.gather()函数,用于并发执行多个异步任务。该函数接受多个异步函数作为参数,并返回一个协程对象。
示例:

import asyncio

async def task1():
    await asyncio.sleep(1)
    return 'Task 1'

async def task2():
    await asyncio.sleep(2)
    return 'Task 2'

async def main():
    results = await asyncio.gather(task1(), task2())
    print(results)

asyncio.run(main())

使用异步上下文管理器:Asynico提供了async with语法来使用异步上下文管理器,类似于使用with语法的同步上下文管理器。异步上下文管理器允许在进入和离开上下文时执行异步操作。
示例:

import asyncio

class MyAsyncContextManager:
    async def __aenter__(self):
        await asyncio.sleep(1)
        print('Entering context')
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await asyncio.sleep(1)
        print('Exiting context')

async def main():
    async with MyAsyncContextManager():
        await asyncio.sleep(2)
        print('Inside context')

asyncio.run(main())

这些是使用Asynico的最佳实践,可以帮助你更好地进行异步编程。请根据你的具体需求和场景进行适当的调整和使用。

posted @ 2023-06-15 13:09  stephen_zuo  阅读(466)  评论(0)    收藏  举报