¿Grupo de subprocesos similar al grupo de multiprocesamiento?


347

¿Existe una clase Pool para subprocesos de trabajo , similar a la clase Pool del módulo de multiprocesamiento ?

Me gusta, por ejemplo, la forma fácil de paralelizar una función de mapa

def long_running_func(p):
    c_func_no_gil(p)

p = multiprocessing.Pool(4)
xs = p.map(long_running_func, range(100))

Sin embargo, me gustaría hacerlo sin la sobrecarga de crear nuevos procesos.

Sé sobre el GIL. Sin embargo, en mi caso de uso, la función será una función C ligada a IO para la cual el contenedor de Python liberará el GIL antes de la llamada a la función real.

¿Tengo que escribir mi propio grupo de subprocesos?



1
Hoy en día es una función de: from multiprocessing.pool import ThreadPool.
Martineau

¿Puedes dar más detalles sobre esto I know about the GIL. However, in my usecase, the function will be an IO-bound C function for which the python wrapper will release the GIL before the actual function call.?
mrgloom

Respuestas:


448

Me acabo de enterar de que en realidad hay una interfaz Pool basada en hilos en el multiprocessingmódulo, sin embargo, está algo oculta y no está debidamente documentada.

Se puede importar a través de

from multiprocessing.pool import ThreadPool

Se implementa utilizando una clase de proceso ficticia que envuelve un hilo de Python. Se puede encontrar esta clase de proceso basada en subprocesos, multiprocessing.dummyque se menciona brevemente en los documentos . Este módulo ficticio supuestamente proporciona toda la interfaz de multiprocesamiento basada en subprocesos.


55
Eso es genial. Tuve un problema al crear ThreadPools fuera del hilo principal, aunque puedes usarlos desde un hilo secundario una vez creado. Puse un problema: bugs.python.org/issue10015
Olson

82
No entiendo por qué esta clase no tiene documentación. Tales clases de ayuda son tan importantes hoy en día.
Wernight

18
@Wernight: no es público principalmente porque nadie ha ofrecido un parche que lo proporcione (o algo similar) como subprocesos. ThreadPool, incluida la documentación y las pruebas. De hecho, sería una buena batería incluirla en la biblioteca estándar, pero no sucederá si nadie la escribe. Una buena ventaja de esta implementación existente en el multiprocesamiento es que debería hacer que cualquier parche de subprocesamiento sea mucho más fácil de escribir ( docs.python.org/devguide )
ncoghlan

3
@ daniel.gindi: multiprocessing.dummy.Pool/ multiprocessing.pool.ThreadPoolson lo mismo, y ambos son grupos de subprocesos. Imitan la interfaz de un grupo de procesos, pero se implementan completamente en términos de subprocesos. Vuelve a leer los documentos, lo tienes al revés.
ShadowRanger

99
@ daniel.gindi: Leer más : " multiprocessing.dummyreplica la API de multiprocessingpero no es más que un contenedor alrededor del threadingmódulo". multiprocessingen general se trata de procesos, pero para permitir el cambio entre procesos y subprocesos, ellos (principalmente) replicaron la multiprocessingAPI multiprocessing.dummy, pero respaldados con subprocesos, no procesos. El objetivo es permitirle import multiprocessing.dummy as multiprocessingcambiar el código basado en procesos a hilos.
ShadowRanger

236

En Python 3 puedes usar concurrent.futures.ThreadPoolExecutor, es decir:

executor = ThreadPoolExecutor(max_workers=10)
a = executor.submit(my_function)

Consulte los documentos para obtener más información y ejemplos.


66
para usar el módulo de futuros con sudo pip install futures
respaldo

es la forma más eficiente y rápida para el procesamiento múltiple
Haritsinh Gohil

2
¿Cuál es la diferencia entre usar ThreadPoolExecutory multiprocessing.dummy.Pool?
Jay

2
from concurrent.futures import ThreadPoolExecutor
stackOverlord

63

Sí, y parece tener (más o menos) la misma API.

import multiprocessing

def worker(lnk):
    ....    
def start_process():
    .....
....

if(PROCESS):
    pool = multiprocessing.Pool(processes=POOL_SIZE, initializer=start_process)
else:
    pool = multiprocessing.pool.ThreadPool(processes=POOL_SIZE, 
                                           initializer=start_process)

pool.map(worker, inputs)
....

99
La ruta de importación para ThreadPooles diferente de Pool. La importación correcta es from multiprocessing.pool import ThreadPool.
Marigold

2
Curiosamente, esta no es una API documentada, y multiprocessing.pool solo se menciona brevemente como que proporciona AsyncResult. Pero está disponible en 2.xy 3.x.
Marvin

2
Esto es lo que estaba buscando. Es solo una línea de importación y un pequeño cambio en mi línea de grupo existente y funciona perfectamente.
Danegraphics

39

Para algo muy simple y ligero (ligeramente modificado desde aquí ):

from Queue import Queue
from threading import Thread


class Worker(Thread):
    """Thread executing tasks from a given tasks queue"""
    def __init__(self, tasks):
        Thread.__init__(self)
        self.tasks = tasks
        self.daemon = True
        self.start()

    def run(self):
        while True:
            func, args, kargs = self.tasks.get()
            try:
                func(*args, **kargs)
            except Exception, e:
                print e
            finally:
                self.tasks.task_done()


class ThreadPool:
    """Pool of threads consuming tasks from a queue"""
    def __init__(self, num_threads):
        self.tasks = Queue(num_threads)
        for _ in range(num_threads):
            Worker(self.tasks)

    def add_task(self, func, *args, **kargs):
        """Add a task to the queue"""
        self.tasks.put((func, args, kargs))

    def wait_completion(self):
        """Wait for completion of all the tasks in the queue"""
        self.tasks.join()

if __name__ == '__main__':
    from random import randrange
    from time import sleep

    delays = [randrange(1, 10) for i in range(100)]

    def wait_delay(d):
        print 'sleeping for (%d)sec' % d
        sleep(d)

    pool = ThreadPool(20)

    for i, d in enumerate(delays):
        pool.add_task(wait_delay, d)

    pool.wait_completion()

Para admitir devoluciones de llamada al finalizar la tarea, solo puede agregar la devolución de llamada a la tupla de la tarea.


¿Cómo pueden unirse los hilos si tienen un bucle infinito incondicional?
Joseph Garvin

@JosephGarvin Lo he probado, y los subprocesos siguen bloqueándose en una cola vacía (ya que la llamada a Queue.get()está bloqueando) hasta que finaliza el programa, después de lo cual finalizan automáticamente.
forumulator

@JosephGarvin, buena pregunta. Queue.join()en realidad se unirá a la cola de tareas, no a subprocesos de trabajo. Entonces, cuando la cola está vacía, wait_completionel sistema operativo cosecha los subprocesos y el subproceso.
randomir

Si todo este código está envuelto en una función ordenada, no parece estar deteniendo los subprocesos incluso cuando la cola está vacía y pool.wait_completion()regresa. El resultado es que los hilos siguen construyéndose.
ubiquibacon

17

Hola, para usar el grupo de subprocesos en Python, puede usar esta biblioteca:

from multiprocessing.dummy import Pool as ThreadPool

y para su uso, esta biblioteca hace así:

pool = ThreadPool(threads)
results = pool.map(service, tasks)
pool.close()
pool.join()
return results

Los subprocesos son la cantidad de subprocesos que desea y las tareas son una lista de tareas que la mayoría asigna al servicio.


Gracias, esa es una gran sugerencia! De los documentos: multiprocessing.dummy replica la API de multiprocesamiento, pero no es más que un contenedor alrededor del módulo de subprocesos. Una corrección - creo que quiere decir que la piscina es api (función, iterable)
layser

2
Nos perdimos los .close()y .join()las llamadas y que las causas .map()para terminar antes hayan terminado todos los hilos. Sólo una advertencia.
Anatoly Scherbakov

8

Aquí está el resultado que finalmente terminé usando. Es una versión modificada de las clases de dgorissen anterior.

Expediente: threadpool.py

from queue import Queue, Empty
import threading
from threading import Thread


class Worker(Thread):
    _TIMEOUT = 2
    """ Thread executing tasks from a given tasks queue. Thread is signalable, 
        to exit
    """
    def __init__(self, tasks, th_num):
        Thread.__init__(self)
        self.tasks = tasks
        self.daemon, self.th_num = True, th_num
        self.done = threading.Event()
        self.start()

    def run(self):       
        while not self.done.is_set():
            try:
                func, args, kwargs = self.tasks.get(block=True,
                                                   timeout=self._TIMEOUT)
                try:
                    func(*args, **kwargs)
                except Exception as e:
                    print(e)
                finally:
                    self.tasks.task_done()
            except Empty as e:
                pass
        return

    def signal_exit(self):
        """ Signal to thread to exit """
        self.done.set()


class ThreadPool:
    """Pool of threads consuming tasks from a queue"""
    def __init__(self, num_threads, tasks=[]):
        self.tasks = Queue(num_threads)
        self.workers = []
        self.done = False
        self._init_workers(num_threads)
        for task in tasks:
            self.tasks.put(task)

    def _init_workers(self, num_threads):
        for i in range(num_threads):
            self.workers.append(Worker(self.tasks, i))

    def add_task(self, func, *args, **kwargs):
        """Add a task to the queue"""
        self.tasks.put((func, args, kwargs))

    def _close_all_threads(self):
        """ Signal all threads to exit and lose the references to them """
        for workr in self.workers:
            workr.signal_exit()
        self.workers = []

    def wait_completion(self):
        """Wait for completion of all the tasks in the queue"""
        self.tasks.join()

    def __del__(self):
        self._close_all_threads()


def create_task(func, *args, **kwargs):
    return (func, args, kwargs)

Para usar la piscina

from random import randrange
from time import sleep

delays = [randrange(1, 10) for i in range(30)]

def wait_delay(d):
    print('sleeping for (%d)sec' % d)
    sleep(d)

pool = ThreadPool(20)
for i, d in enumerate(delays):
    pool.add_task(wait_delay, d)
pool.wait_completion()

Anotación para otros lectores: este código es Python 3 (shebang #!/usr/bin/python3)
Daniel Marschall

¿Por qué usas for i, d in enumerate(delays):y luego ignoras el ivalor?
Martineau

@martineau: probablemente solo una reliquia del desarrollo donde probablemente querían imprimir idurante una carrera.
n1k31t4

Por que create_taskhay ¿Para qué sirve?
MrR

No puedo creer y responder con 4 votos en SO es la forma de hacer ThreadPooling en Python. ¿El Threadpool en la distribución oficial de Python todavía está roto? ¿Qué me estoy perdiendo?
MrR

2

La sobrecarga de crear los nuevos procesos es mínima, especialmente cuando solo son 4 de ellos. Dudo que este sea un punto clave de rendimiento de su aplicación. Manténgalo simple, optimice dónde tiene que hacerlo y hacia dónde apuntan los resultados del perfil.


55
Si el interrogador está bajo Windows (que no creo que haya especificado), entonces creo que la separación del proceso puede ser un gasto significativo. Al menos está en los proyectos que he estado haciendo recientemente. :-)
Brandon Rhodes

1

No hay una agrupación integrada basada en subprocesos. Sin embargo, puede ser muy rápido implementar una cola de productor / consumidor con la Queueclase.

De: https://docs.python.org/2/library/queue.html

from threading import Thread
from Queue import Queue
def worker():
    while True:
        item = q.get()
        do_work(item)
        q.task_done()

q = Queue()
for i in range(num_worker_threads):
     t = Thread(target=worker)
     t.daemon = True
     t.start()

for item in source():
    q.put(item)

q.join()       # block until all tasks are done

3
Este ya no es el caso con el concurrent.futuresmódulo.
Thanatos

11
No creo que esto sea cierto en absoluto. from multiprocessing.pool import ThreadPool
Randall Hunt


0

Otra forma puede ser agregar el proceso al grupo de colas de hilos

import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=cpus) as executor:
    for i in range(0, len(list_of_files) - 1):
        a = executor.submit(loop_files2, i, list_of_files2, mt_list, temp_path, mt_dicto)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.