Conjunto de threads semelhante ao pool de multiprocessamento?

347

Existe uma classe Pool para threads de trabalho , semelhante à classe Pool do módulo de multiprocessamento ?

Eu gosto, por exemplo, da maneira fácil de paralelizar uma função de mapa

def long_running_func(p):
    c_func_no_gil(p)

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

no entanto, eu gostaria de fazê-lo sem a sobrecarga de criar novos processos.

Eu sei sobre o GIL. No entanto, no meu caso de usuário, a função será uma função C vinculada a IO para a qual o wrapper python liberará o GIL antes da chamada de função real.

Eu tenho que escrever minha própria piscina de threading?

Martin
fonte
Aqui está algo que parece promissor no Python Cookbook: Receita 576519: Pool de threads com a mesma API do processamento (multi)
.Pool
11
Hoje em dia ele é construído-in: from multiprocessing.pool import ThreadPool.
martineau
Você pode elaborar isso 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

Respostas:

448

Acabei de descobrir que realmente existe uma interface Pool baseada em encadeamento no multiprocessingmódulo, no entanto, ela está oculta e não está documentada corretamente.

Pode ser importado via

from multiprocessing.pool import ThreadPool

Ele é implementado usando uma classe fictícia Process que envolve um encadeamento python. Essa classe Process baseada em encadeamento pode ser encontrada na multiprocessing.dummyqual é mencionada brevemente nos documentos . Este módulo fictício supostamente fornece toda a interface de multiprocessamento baseada em threads.

Martin
fonte
5
Fantástico. Eu tive um problema ao criar ThreadPools fora do thread principal, você pode usá-los a partir de um thread filho, uma vez criado. Eu coloquei um problema para isso: bugs.python.org/issue10015
Olson
82
Não entendo por que essa classe não tem documentação. Tais classes auxiliares são tão importantes hoje em dia.
Wernight 15/10/12
18
@ À noite: não é público principalmente porque ninguém ofereceu um patch que o fornece (ou algo semelhante) como threading.ThreadPool, incluindo documentação e testes. Seria realmente uma boa bateria incluir na biblioteca padrão, mas isso não acontecerá se ninguém a escrever. Uma boa vantagem desta implementação existente em multiprocessamento, é que ele deve fazer qualquer patch de segmentação muito mais fácil de escrever ( docs.python.org/devguide )
ncoghlan
3
@ daniel.gindi: multiprocessing.dummy.Pool/ multiprocessing.pool.ThreadPoolsão a mesma coisa e são ambos conjuntos de threads. Eles imitam a interface de um pool de processos, mas são implementados inteiramente em termos de encadeamento. Releia os documentos, você conseguiu o contrário.
precisa
9
@ daniel.gindi: Leia mais : " multiprocessing.dummyreplica a API, multiprocessingmas não passa de um invólucro ao redor do threadingmódulo". multiprocessingem geral, trata-se de processos, mas, para permitir a alternância entre processos e encadeamentos, eles (principalmente) replicaram a multiprocessingAPI multiprocessing.dummy, mas fizeram backup com encadeamentos, não processos. O objetivo é permitir que você import multiprocessing.dummy as multiprocessingaltere o código baseado em processo para baseado em encadeamento.
precisa
236

No Python 3 você pode usar concurrent.futures.ThreadPoolExecutor, ou seja:

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

Veja os documentos para mais informações e exemplos.

Adrian Adamiak
fonte
6
para usar o módulo de futuros portados, executesudo pip install futures
yair
é a maneira mais eficiente e mais rápido para processamento multi
Haritsinh Gohil
2
Qual é a diferença entre using ThreadPoolExecutore multiprocessing.dummy.Pool?
Jay
2
de concurrent.futures import ThreadPoolExecutor
stackOverlord
63

Sim, e parece ter (mais ou menos) a mesma 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)
....
warfares
fonte
9
O caminho de importação para ThreadPoolé diferente de Pool. A importação correta é from multiprocessing.pool import ThreadPool.
Marigold
2
Estranhamente, essa não é uma API documentada e o multiprocessing.pool é brevemente mencionado como fornecendo AsyncResult. Mas está disponível nas versões 2.xe 3.x.
Marvin
2
Era isso que eu estava procurando. É apenas uma única linha de importação e uma pequena alteração na minha linha de pool existente e funciona perfeitamente.
Danegraphics
39

Para algo muito simples e leve (ligeiramente modificado a partir daqui ):

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 oferecer suporte a retornos de chamada na conclusão da tarefa, basta adicionar o retorno de chamada à tupla da tarefa.

dgorissen
fonte
como os threads podem se unir se tiverem um loop incondicionalmente infinito?
Joseph Garvin
@JosephGarvin Eu testei e os threads continuam bloqueando uma fila vazia (desde a chamada para Queue.get() está bloqueando) até o programa terminar, após o que são finalizados automaticamente.
forumulator 22/03
@JosephGarvin, boa pergunta. Queue.join()realmente ingressará na fila de tarefas, não nos threads de trabalho. Portanto, quando a fila está vazia, os wait_completionretornos, o programa são finalizados e os threads são colhidos pelo sistema operacional.
randomir
Se todo esse código estiver agrupado em uma função pura, ele não parará os threads, mesmo quando a fila estiver vazia e pool.wait_completion()retornar. O resultado é que os threads continuam construindo.
ubiquibacon 9/01/19
17

Olá, para usar o pool de threads no Python, você pode usar esta biblioteca:

from multiprocessing.dummy import Pool as ThreadPool

e, para uso, esta biblioteca faz o seguinte:

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

Os threads são o número de threads que você deseja e as tarefas são uma lista das tarefas que mais são mapeadas para o serviço.

Manochehr Rasouli
fonte
Obrigado, essa é uma ótima sugestão! Nos documentos: multiprocessing.dummy, replica a API do multiprocessamento, mas não passa de um invólucro em torno do módulo de segmentação. Uma correção - eu acho que você quer dizer que a API do pool é (função, iterável) #
layser:
2
Perdemos os .close()e .join()chamadas e que as causas .map()para terminar antes de todos os fios estão acabados. Apenas um aviso.
Anatoly Scherbakov
8

Aqui está o resultado que finalmente acabei usando. É uma versão modificada das classes por dgorissen acima.

Arquivo: 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 a 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()
forumulador
fonte
Annotion para outros leitores: Este código é Python 3 (shebang #!/usr/bin/python3)
Daniel Marschall
Por que você usa for i, d in enumerate(delays):e depois ignora o ivalor?
27518
@martineau - provavelmente apenas uma relíquia do desenvolvimento, onde eles provavelmente queriam imprimir idurante uma execução.
N1k31t4 18/1018
Por que create_taskexiste? Para que serve?
MRR
Eu não posso acreditar e responder com 4 votos no SO é a maneira de fazer ThreadPooling em Python. O Threadpool na distribuição oficial de python ainda está quebrado? o que estou perdendo?
MrR 02/04/19
2

A sobrecarga de criação dos novos processos é mínima, especialmente quando são apenas quatro deles. Duvido que este seja um hot spot de desempenho do seu aplicativo. Mantenha-o simples, otimize para onde você precisa e para onde os resultados de criação de perfil apontam.

incrédulo
fonte
5
Se o interlocutor estiver no Windows (que eu não acredito que ele especificou), acho que o processo de spin-off pode ser uma despesa significativa. Pelo menos é nos projetos que venho fazendo recentemente. :-)
Brandon Rhodes
1

Não existe um pool baseado em encadeamento. No entanto, pode ser muito rápido implementar uma fila de produtores / consumidores com a Queueclasse.

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
Yann Ramin
fonte
3
Este não é mais o caso do concurrent.futuresmódulo.
Thanatos
11
Acho que isso não é mais verdade. from multiprocessing.pool import ThreadPool
Randall Hunt
0

outra maneira pode ser adicionar o processo ao pool de filas

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)
pelos
fonte