Python: combinación de multiprocesamiento y Asyncio para mejorar el rendimiento

¡Haz una fortuna con tu pequeña mano, dale un pulgar hacia arriba!

Introducción

Gracias a GIL, el uso de múltiples subprocesos para realizar tareas intensivas de CPU nunca fue una opción. Con la popularidad de las CPU multinúcleo, Python proporciona una solución de multiprocesamiento para realizar tareas intensivas de CPU. Pero hasta ahora, todavía hay algunos problemas en el uso directo de API relacionadas con procesos múltiples.

Antes de comenzar este artículo [1] , también tenemos un pequeño fragmento de código para ayudar a demostrar:

import time
from multiprocessing import Process


def sum_to_num(final_num: int) -> int:
    start = time.monotonic()

    result = 0
    for i in range(0, final_num+11):
        result += i

    print(f"The method with {final_num} completed in {time.monotonic() - start:.2f} second(s).")
    return result

Este método toma un parámetro y lo incrementa a partir de 0. Imprime el tiempo de ejecución del método y devuelve el resultado.

Problemas con múltiples procesos.

def main():
    # We initialize the two processes with two parameters, from largest to smallest
    process_a = Process(target=sum_to_num, args=(200_000_000,))
    process_b = Process(target=sum_to_num, args=(50_000_000,))

    # And then let them start executing
    process_a.start()
    process_b.start()

    # Note that the join method is blocking and gets results sequentially
    start_a = time.monotonic()
    process_a.join()
    print(f"Process_a completed in {time.monotonic() - start_a:.2f} seconds")

    # Because when we wait process_a for join. The process_b has joined already.
    # so the time counter is 0 seconds.
    start_b = time.monotonic()
    process_b.join()
    print(f"Process_b completed in {time.monotonic() - start_b:.2f} seconds")

Como se muestra en el código, creamos e iniciamos directamente varios procesos y llamamos a los métodos de inicio y unión de cada proceso. Sin embargo, hay algunos problemas aquí:

  1. El método de unión no puede devolver el resultado de la ejecución de la tarea.
  2. El método de unión bloquea el proceso principal y lo ejecuta secuencialmente.

Aunque las tareas posteriores se ejecutan más rápido que las anteriores, como se muestra en el siguiente diagrama:

alt
alt

Problemas con el uso de la piscina

Si usamos multiprocessing.Pool, también hay algunos problemas:

def main():
    with Pool() as pool:
        result_a = pool.apply(sum_to_num, args=(200_000_000,))
        result_b = pool.apply(sum_to_num, args=(50_000_000,))

        print(f"sum_to_num with 200_000_000 got a result of {result_a}.")
        print(f"sum_to_num with 50_000_000 got a result of {result_b}.")

Como muestra el código, el método de aplicación de Pool es síncrono, lo que significa que debe esperar a que se complete la tarea de aplicación anterior antes de iniciar la siguiente tarea de aplicación.

alt

Por supuesto, podemos usar el método apply_async para crear tareas de forma asíncrona. Pero nuevamente, debe usar el método get para obtener el bloqueo de resultados. Nos lleva de vuelta al problema del método de unión:

def main():
    with Pool() as pool:
        result_a = pool.apply_async(sum_to_num, args=(200_000_000,))
        result_b = pool.apply_async(sum_to_num, args=(50_000_000,))

        print(f"sum_to_num with 200_000_000 got a result of {result_a.get()}.")
        print(f"sum_to_num with 50_000_000 got a result of {result_b.get()}.")
alt

Problemas con el uso de ProcessPoolExecutor directamente

Entonces, ¿qué pasa si usamos concurrent.futures.ProcesssPoolExecutor para ejecutar nuestras tareas vinculadas a la CPU?

def main():
    with ProcessPoolExecutor() as executor:
        numbers = [200_000_000, 50_000_000]
        for result in executor.map(sum_to_num, numbers):
            print(f"sum_to_num got a result which is {result}.")

Como puede ver en el código, todo se ve muy bien y se llama como asyncio.as_completed. Pero mire los resultados; todavía se obtienen en el orden de inicio. Esto es bastante diferente de asyncio.as_completed, que obtiene los resultados en orden de ejecución:

alt
alt

Use la corrección run_in_executor de asyncio

幸运的是,我们可以使用 asyncio 来处理 IO-bound 任务,它的 run_in_executor 方法可以像 asyncio 一样调用多进程任务。不仅统一了并发和并行的API,还解决了我们上面遇到的各种问题:

async def main():
    loop = asyncio.get_running_loop()
    tasks = []

    with ProcessPoolExecutor() as executor:
        for number in [200_000_000, 50_000_000]:
            tasks.append(loop.run_in_executor(executor, sum_to_num, number))
        
        # Or we can just use the method asyncio.gather(*tasks)
        for done in asyncio.as_completed(tasks):
            result = await done
            print(f"sum_to_num got a result which is {result}")
alt

由于上一篇的示例代码都是模拟我们应该调用的并发过程的方法,所以很多读者在学习之后在实际编码中还是需要帮助理解如何使用。所以在了解了为什么我们需要在asyncio中执行CPU-bound并行任务之后,今天我们将通过一个真实世界的例子来解释如何使用asyncio同时处理IO-bound和CPU-bound任务,并领略asyncio对我们的效率代码。

Reference

[1]

Source: https://towardsdatascience.com/combining-multiprocessing-and-asyncio-in-python-for-performance-boosts-15496ffe96b

本文由 mdnice 多平台发布

Supongo que te gusta

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