Use estos métodos para hacer que sus tareas simultáneas de Python funcionen mejor

Mueve tu manita para hacer una fortuna, ¡dale un pulgar hacia arriba!

pregunta

Durante mucho tiempo, el rendimiento de subprocesos múltiples de Python no ha cumplido las expectativas debido a la GIL.

Entonces, a partir de la versión 3.4, Python introdujo el paquete asyncio para ejecutar tareas vinculadas a IO simultáneamente de manera simultánea. Después de varias iteraciones, el efecto de la API de asyncio es muy bueno y el rendimiento de las tareas simultáneas ha mejorado mucho en comparación con la versión de subprocesos múltiples.

Sin embargo, los programadores aún cometen muchos errores al usar asyncio:

En la siguiente figura se muestra un error. Use directamente el método de rutina await para cambiar la llamada a tareas simultáneas de asincrónicas a síncronas y, finalmente, pierda la función de simultaneidad.

async def main():
    result_1 = await some_coro("name-1")
    result_2 = await some_coro("name-2")

Otro error se muestra en la imagen a continuación, aunque el programador se da cuenta de que necesita usar create_task para crear una tarea para ejecutar en segundo plano. El siguiente método de esperar las tareas una por una convierte las tareas de diferente tiempo en una espera ordenada.

async def main():
    task_1 = asyncio.create_task(some_coro("name-1"))
    task_2 = asyncio.create_task(some_coro("name-2"))
    
    result_1 = await task_1
    result_2 = await task_2

Este código esperará a que task_1 finalice primero, independientemente de si task_2 finaliza primero.

¿Qué es la ejecución de tareas concurrentes?

Entonces, ¿qué es una tarea concurrente real? Ilustremos con una imagen:

alt

Como se muestra en la figura, un proceso simultáneo debe constar de dos partes: iniciar la tarea en segundo plano, volver a unir la tarea en segundo plano a la función principal y obtener el resultado.

La mayoría de los lectores ya saben cómo iniciar tareas en segundo plano con create_task. Hoy, cubriré varias formas de esperar a que se completen las tareas en segundo plano y las mejores prácticas para cada una.

comenzar

Antes de comenzar a presentar al protagonista de hoy, debemos preparar un método asíncrono de ejemplo para simular llamadas de método vinculadas a IO y una excepción AsyncException personalizada, que se puede usar para solicitar información de excepción amigable cuando la prueba arroja una excepción:

from random import random, randint
import asyncio


class AsyncException(Exception):
    def __init__(self, message, *args, **kwargs):
        self.message = message
        super(*args, **kwargs)

    def __str__(self):
        return self.message


async def some_coro(name):
    print(f"Coroutine {name} begin to run")
    value = random()

    delay = randint(14)
    await asyncio.sleep(delay)
    if value > 0.5:
        raise AsyncException(f"Something bad happen after delay {delay} second(s)")
    print(f"Coro {name} is Done. with delay {delay} second(s)")
    return value

Comparación de métodos de ejecución simultánea

1. asyncio.reunir

asyncio.gather se puede usar para iniciar un grupo de tareas en segundo plano, esperar a que terminen de ejecutarse y obtener una lista de los resultados:

async def main():
    aws, results = [], []
    for i in range(3):
        aws.append(asyncio.create_task(some_coro(f'name-{i}')))

    results = await asyncio.gather(*aws)  # need to unpack the list
    for result in results:
        print(f">got : {result}")

asyncio.run(main())

asyncio.gather, al componer un grupo de tareas en segundo plano, no puede aceptar directamente una lista o colección como argumento. Descomprima si necesita pasar una lista que contenga tareas en segundo plano.

asyncio.gather acepta un parámetro return_exceptions. Cuando el valor de return_exception es False, cualquier excepción lanzada por la tarea en segundo plano se lanzará al autor de la llamada del método de recopilación. La lista de resultados del método de recopilación está vacía.

async def main():
    aws, results = [], []
    for i in range(3):
        aws.append(asyncio.create_task(some_coro(f'name-{i}')))

    try:
        results = await asyncio.gather(*aws, return_exceptions=False)  # need to unpack the list
    except AsyncException as e:
        print(e)
    for result in results:
        print(f">got : {result}")

asyncio.run(main())
alt

当return_exception的值为True时,后台任务抛出的异常不会影响其他任务的执行,最终会合并到结果列表中一起返回。

results = await asyncio.gather(*aws, return_exceptions=True)
alt

接下来我们看看为什么gather方法不能直接接受一个列表,而是要对列表进行解包。因为当一个列表被填满并执行时,我们很难在等待任务完成时向列表中添加新任务。但是 gather 方法可以使用嵌套组将现有任务与新任务混合,解决了中间无法添加新任务的问题:

async def main():
    aws, results = [], []
    for i in range(3):
        aws.append(asyncio.create_task(some_coro(f'name-{i}')))
    group_1 = asyncio.gather(*aws)  # note we don't use await now
    # when some situation happen, we may add a new task
    group_2 = asyncio.gather(group_1, asyncio.create_task(some_coro("a new task")))
    results = await group_2
    for result in results:
        print(f">got : {result}")

asyncio.run(main())

但是gather不能直接设置timeout参数。如果需要为所有正在运行的任务设置超时时间,就用这个姿势,不够优雅。

async def main():
    aws, results = [], []
    for i in range(3):
        aws.append(asyncio.create_task(some_coro(f'name-{i}')))

    results = await asyncio.wait_for(asyncio.gather(*aws), timeout=2)
    for result in results:
        print(f">got : {result}")

asyncio.run(main())

2. asyncio.as_completed

有时,我们必须在完成一个后台任务后立即开始下面的动作。比如我们爬取一些数据,马上调用机器学习模型进行计算,gather方法不能满足我们的需求,但是我们可以使用as_completed方法。

在使用 asyncio.as_completed 方法之前,我们先看一下这个方法的源码。

# This is *not* a @coroutine!  It is just an iterator (yielding Futures).
def as_completed(fs, *, timeout=None):
  # ...
  for f in todo:
      f.add_done_callback(_on_completion)
  if todo and timeout is not None:
      timeout_handle = loop.call_later(timeout, _on_timeout)
  for _ in range(len(todo)):
      yield _wait_for_one()

源码显示as_completed不是并发方法,返回一个带有yield语句的迭代器。所以我们可以直接遍历每个完成的后台任务,我们可以对每个任务单独处理异常,而不影响其他任务的执行:

async def main():
    aws = []
    for i in range(5):
        aws.append(asyncio.create_task(some_coro(f"name-{i}")))

    for done in asyncio.as_completed(aws):  # we don't need to unpack the list
        try:
            result = await done
            print(f">got : {result}")
        except AsyncException as e:
            print(e)

asyncio.run(main())

as_completed 接受超时参数,超时后当前迭代的任务会抛出asyncio.TimeoutError:

async def main():
    aws = []
    for i in range(5):
        aws.append(asyncio.create_task(some_coro(f"name-{i}")))

    for done in asyncio.as_completed(aws, timeout=2):  # we don't need to unpack the list
        try:
            result = await done
            print(f">got : {result}")
        except AsyncException as e:
            print(e)
        except asyncio.TimeoutError: # we need to handle the TimeoutError
            print("time out.")

asyncio.run(main())
alt

as_complete在处理任务执行的结果方面比gather灵活很多,但是在等待的时候很难往原来的任务列表中添加新的任务。

3. asyncio.wait

asyncio.wait 的调用方式与 as_completed 相同,但返回一个包含两个集合的元组:done 和 pending。 done 保存已完成执行的任务,而 pending 保存仍在运行的任务。

asyncio.wait 接受一个 return_when 参数,它可以取三个枚举值:

  • 当return_when为asyncio.ALL_COMPLETED时,done存放所有完成的任务,pending为空。
  • 当 return_when 为 asyncio.FIRST_COMPLETED 时,done 持有所有已完成的任务,而 pending 持有仍在运行的任务。
async def main():
    aws = set()
    for i in range(5):
        aws.add(asyncio.create_task(some_coro(f"name-{i}")))

    done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED)
    for task in done:
        try:
            result = await task
            print(f">got : {result}")
        except AsyncException as e:
            print(e)
    print(f"the length of pending is {len(pending)}")

asyncio.run(main())
alt
  • 当return_when为asyncio.FIRST_EXCEPTION时,done存放抛出异常并执行完毕的任务,pending存放仍在运行的任务。

当 return_when 为 asyncio.FIRST_COMPLETED 或 asyncio.FIRST_EXECEPTION 时,我们可以递归调用 asyncio.wait,这样我们就可以添加新的任务,并根据情况一直等待所有任务完成。

async def main():
    pending = set()
    for i in range(5):
        pending.add(asyncio.create_task(some_coro(f"name-{i}")))  # note the type and name of the task list

    while pending:
        done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_EXCEPTION)
        for task in done:
            try:
                result = await task
                print(f">got : {result}")
            except AsyncException as e:
                print(e)
                pending.add(asyncio.create_task(some_coro("a new task")))
    print(f"the length of pending is {len(pending)}")

asyncio.run(main())
alt

4. asyncio.TaskGroup

在 Python 3.11 中,asyncio 引入了新的 TaskGroup API,正式让 Python 支持结构化并发。此功能允许您以更 Pythonic 的方式管理并发任务的生命周期。

总结

本文[1]介绍了 asyncio.gather、asyncio.as_completed 和 asyncio.wait API,还回顾了 Python 3.11 中引入的新 asyncio.TaskGroup 特性。

根据实际需要使用这些后台任务管理方式可以让我们的asyncio并发编程更加灵活。

Reference

[1]

Source: https://towardsdatascience.com/use-these-methods-to-make-your-python-concurrent-tasks-perform-better-b693b7a633e1

本文由 mdnice 多平台发布

Supongo que te gusta

Origin blog.csdn.net/swindler_ice/article/details/130894975
Recomendado
Clasificación