Concurrent.futures vs Multiprocessing em Python 3

148

O Python 3.2 introduziu o Concurrent Futures , que parece ser uma combinação avançada dos módulos de threading e multiprocessing mais antigos .

Quais são as vantagens e desvantagens de usar isso para tarefas vinculadas à CPU sobre o antigo módulo de multiprocessamento?

Este artigo sugere que eles são muito mais fáceis de trabalhar - é esse o caso?

GIS-Jonathan
fonte

Respostas:

145

Eu não chamaria de concurrent.futuresmais "avançado" - é uma interface mais simples que funciona da mesma forma, independentemente de você usar vários threads ou vários processos como o truque de paralelização subjacente.

Portanto, como praticamente todas as instâncias de "interface mais simples", as mesmas vantagens e desvantagens estão envolvidas: ela possui uma curva de aprendizado mais rasa, em grande parte apenas porque há muito menos disponível para aprender; mas, como oferece menos opções, pode acabar frustrando você de maneiras que as interfaces mais ricas não o farão.

No que diz respeito às tarefas ligadas à CPU, isso é muito subespecificado para dizer muito significativo. Para tarefas ligadas à CPU no CPython, você precisa de vários processos, em vez de vários threads, para ter qualquer chance de obter uma aceleração. Mas quanto você recebe (se houver) de uma aceleração depende dos detalhes do seu hardware, sistema operacional e, principalmente, da quantidade de comunicação entre processos que suas tarefas específicas exigem. Nos bastidores, todos os truques de paralelismo entre processos dependem das mesmas primitivas do sistema operacional - a API de alto nível que você usa para obter essas informações não é o principal fator na velocidade da linha final.

Editar: exemplo

Aqui está o código final mostrado no artigo que você referenciou, mas estou adicionando uma declaração de importação necessária para fazê-la funcionar:

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))}

Aqui está exatamente a mesma coisa usando multiprocessing:

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))}

Observe que a capacidade de usar multiprocessing.Poolobjetos como gerenciadores de contexto foi adicionada no Python 3.3.

Com quem é mais fácil trabalhar? LOL ;-) Eles são essencialmente idênticos.

Uma diferença é que Poolsuporta tantas maneiras diferentes de fazer as coisas que você talvez não perceba o quão fácil pode ser até que você tenha subido bastante a curva de aprendizado.

Novamente, todas essas maneiras diferentes são uma força e uma fraqueza. Eles são fortes porque a flexibilidade pode ser necessária em algumas situações. Eles são uma fraqueza por causa de "de preferência apenas uma maneira óbvia de fazê-lo". Um projeto que adere exclusivamente (se possível) concurrent.futuresprovavelmente será mais fácil de manter a longo prazo, devido à falta de novidades gratuitas sobre como sua API mínima pode ser usada.

Tim Peters
fonte
20
"você precisa de múltiplos processos, em vez de vários threads, para ter alguma chance de acelerar" é muito duro. Se a velocidade é importante; o código já pode usar uma biblioteca C e, portanto, pode liberar o GIL, por exemplo, regex, lxml, numpy.
JFS
4
@ JFSebastian, obrigado por adicionar isso - talvez eu devesse ter dito "sob puro CPython", mas receio que não exista uma maneira curta de explicar a verdade aqui sem discutir o GIL.
Tim Peters
2
E vale ressaltar que os threads podem ser especialmente úteis e suficientes ao operar com E / S longas.
precisa saber é o seguinte
9
@ TimPeters De alguma forma, ProcessPoolExecutorna verdade, tem mais opções do que Poolporque ProcessPoolExecutor.submitretorna Futureinstâncias que permitem cancelamento ( cancel), verificando qual exceção foi gerada ( exception) e adicionando dinamicamente um retorno de chamada a ser chamado após a conclusão ( add_done_callback). Nenhum desses recursos está disponível com AsyncResultinstâncias retornadas por Pool.apply_async. Em outras formas Pooltem mais opções devido a initializer/ initargs, maxtasksperchilde contextna Pool.__init__, e mais métodos expostos por Poolexemplo.
max
2
@max, claro, mas observe que a pergunta não era sobre Pool, era sobre os módulos. Poolé uma pequena parte do conteúdo multiprocessinge está tão distante nos documentos que leva um tempo até que as pessoas percebam que ele existe multiprocessing. Essa resposta em particular foi focada Poolporque esse é todo o artigo ao qual o OP se vinculou e que cfé "muito mais fácil trabalhar" simplesmente não é verdade sobre o que o artigo discutiu. Além disso, cf's as_completed()também pode ser muito útil.
Tim Peters