Programación paralela de ProcessPoolExecutor
Tienes un programa que realiza un trabajo intensivo de CPU y quieres que aproveche las CPU multinúcleo para ejecutarse más rápido.
La biblioteca concurrent.futures proporciona una clase ProcessPoolExecutor que se puede utilizar para ejecutar funciones computacionalmente intensivas en un intérprete de Python independiente. Sin embargo, para usarlo, primero debes realizar algunas tareas computacionales intensivas. Lo demostramos con un ejemplo sencillo y práctico. Suponga que tiene un archivo gzip del directorio de registro del servidor web Apache:
Suponga además que el contenido de cada archivo de registro se parece al siguiente:
Aquí hay un script que busca en estos archivos de registro Muestra todos los hosts que han accedido al archivo robots.txt:
El programa anterior está escrito en el estilo habitual de reducción de mapas. La función find_robots() realiza una operación de mapa en una colección de nombres de archivos y resume los resultados en un solo resultado, que es la colección all_robots en la función find_all_robots(). Ahora, supongamos que desea modificar este programa para utilizar CPU multinúcleo. Es simple: simplemente reemplace la operación map() con una operación similar generada en la biblioteca concurrent.futures. Aquí hay una versión modificada simple:
Después de esta modificación, ejecutar este script produce los mismos resultados, pero es 3,5 veces más rápido en una máquina de cuatro núcleos que antes. El efecto de optimización del rendimiento real varía según la cantidad de CPU de su máquina.
El uso típico de ProcessPoolExecutor es el siguiente:
El principio es que un ProcessPoolExecutor crea N intérpretes Python independientes y N es el número de CPU disponibles en el sistema. Puede modificar la cantidad de procesadores proporcionando parámetros opcionales a ProcessPoolExecutor(N). Este grupo de procesamiento se ejecutará hasta que se ejecute la última declaración del bloque with y luego se cerrará el grupo de procesamiento. Sin embargo, el programa esperará hasta que se haya procesado todo el trabajo enviado.
El trabajo enviado al pool debe definirse como una función. Hay dos formas de enviarlo. Si desea que una operación de comprensión de lista o map() se ejecute en paralelo, use pool.map():
Alternativamente, puede usar pool.submit() para enviar manualmente una sola tarea:
Si envía una tarea manualmente, el resultado es una instancia futura. Para obtener el resultado final, debe llamar a su método result(). Bloquea el proceso hasta que se devuelve el resultado.
Si no desea bloquear, también puede usar una función de devolución de llamada, por ejemplo:
La función de devolución de llamada acepta una instancia futura y se usa para obtener el resultado final ( por ejemplo, llamando a su método resultado()). Aunque los grupos de procesamiento son fáciles de usar, todavía hay muchas cosas a las que prestar atención al diseñar programas grandes, como las siguientes:
No puede controlar ningún comportamiento del proceso hijo una vez iniciado, por lo que es mejor para mantenerlo simple y puro ——La función no debe modificar el entorno.
Clonará el intérprete de Python, incluidos todos los estados del programa en el momento de la bifurcación. En Windows, clonar el intérprete no clona el estado. La operación de bifurcación real ocurre después de la primera llamada a pool.map() o pool.submit().
Debe crear y activar un grupo de procesos antes de crear cualquier hilo (como crear un grupo de procesos en el hilo principal cuando se inicia el programa).