Concurrent.futures vs Multiprocessing in Python 3

Respuestas:

145

No llamaría concurrent.futuresmás "avanzado": es una interfaz más simple que funciona de manera muy similar independientemente de si usa múltiples hilos o múltiples procesos como el truco de paralelización subyacente.

Por lo tanto, como prácticamente todas las instancias de "interfaz más simple", están involucradas las mismas compensaciones: tiene una curva de aprendizaje más superficial, en gran parte solo porque hay mucho menos disponible para aprender; pero, debido a que ofrece menos opciones, eventualmente puede frustrarte de una manera que las interfaces más ricas no lo harán.

En lo que respecta a las tareas vinculadas a la CPU, eso es demasiado subespecificado para decir mucho significativo. Para las tareas vinculadas a la CPU en CPython, necesita múltiples procesos en lugar de múltiples subprocesos para tener alguna posibilidad de acelerar. Pero la cantidad (si la hay) de aceleración que obtiene depende de los detalles de su hardware, su sistema operativo y especialmente de la cantidad de comunicación entre procesos que requieren sus tareas específicas. Debajo de las cubiertas, todos los trucos de paralelización entre procesos dependen de las mismas primitivas del sistema operativo: la API de alto nivel que utiliza para obtener esas no es un factor principal en la velocidad de la línea de fondo.

Editar: ejemplo

Aquí está el código final que se muestra en el artículo al que hizo referencia, pero estoy agregando una declaración de importación necesaria para que funcione:

from concurrent.futures import ProcessPoolExecutor
def pool_factorizer_map(nums, nprocs):
    # Let the executor divide the work among processes by using 'map'.
    with ProcessPoolExecutor(max_workers=nprocs) as executor:
        return {num:factors for num, factors in
                                zip(nums,
                                    executor.map(factorize_naive, nums))}

Aquí está exactamente lo mismo usando en su multiprocessinglugar:

import multiprocessing as mp
def mp_factorizer_map(nums, nprocs):
    with mp.Pool(nprocs) as pool:
        return {num:factors for num, factors in
                                zip(nums,
                                    pool.map(factorize_naive, nums))}

Tenga en cuenta que la capacidad de usar multiprocessing.Poolobjetos como administradores de contexto se agregó en Python 3.3.

¿Con cuál es más fácil trabajar? LOL ;-) Son esencialmente idénticos.

Una diferencia es que Pooladmite tantas formas diferentes de hacer las cosas que es posible que no se dé cuenta de lo fácil que puede ser hasta que haya subido bastante en la curva de aprendizaje.

Nuevamente, todas esas formas diferentes son tanto una fortaleza como una debilidad. Son una fortaleza porque la flexibilidad puede ser necesaria en algunas situaciones. Son una debilidad debido a "preferiblemente solo una forma obvia de hacerlo". Un proyecto que se concurrent.futuresmantenga exclusivamente (si es posible) probablemente será más fácil de mantener a largo plazo, debido a la falta de novedad gratuita sobre cómo se puede usar su API mínima.

Tim Peters
fuente
20
"necesitas múltiples procesos en lugar de múltiples hilos para tener alguna posibilidad de acelerar" es demasiado duro. Si la velocidad es importante; el código ya puede usar una biblioteca C y, por lo tanto, puede liberar GIL, por ejemplo, regex, lxml, numpy.
jfs
44
@JFSebastian, gracias por agregar eso, tal vez debería haber dicho "bajo CPython puro ", pero me temo que no hay una manera corta de explicar la verdad aquí sin discutir el GIL.
Tim Peters
2
Y vale la pena mencionar que los subprocesos pueden ser especialmente útiles y suficientes cuando se opera con E / S largas.
kotrfa
9
@TimPeters De alguna manera, en ProcessPoolExecutorrealidad tiene más opciones que Poolporque ProcessPoolExecutor.submitdevuelve Futureinstancias que permiten la cancelación ( cancel), verifica qué excepción se produjo ( exception) y agrega dinámicamente una devolución de llamada para que se invoque al finalizar ( add_done_callback). Ninguna de estas características está disponible con AsyncResultinstancias devueltas por Pool.apply_async. En otras formas Pooltiene más opciones debido a initializer/ initargs, maxtasksperchildy contexten Pool.__init__, y más métodos expuestos por Poolejemplo.
max
2
@max, claro, pero tenga en cuenta que la pregunta no Poolera sobre los módulos. Pooles una pequeña parte de lo que hay dentro multiprocessingy está tan abajo en los documentos que la gente tarda un tiempo en darse cuenta de que incluso existe multiprocessing. Esta respuesta particular se centró en Poolporque ese es todo el artículo al que se vinculó el OP, y que cfes "mucho más fácil trabajar con él" simplemente no es cierto sobre lo que el artículo discutió. Más allá de eso, cf's as_completed()también puede ser muy útil.
Tim Peters