decidindo entre subprocesso, multiprocessamento e thread em Python?

110

Gostaria de paralelizar meu programa Python para que ele possa usar vários processadores na máquina em que é executado. Minha paralelização é muito simples, em que todas as "threads" paralelas do programa são independentes e gravam sua saída em arquivos separados. Não preciso dos threads para trocar informações, mas é fundamental que eu saiba quando os threads terminam, pois algumas etapas do meu pipeline dependem de sua saída.

A portabilidade é importante, pois gostaria que fosse executado em qualquer versão do Python no Mac, Linux e Windows. Dadas essas restrições, qual é o módulo Python mais apropriado para implementar isso? Estou tentando decidir entre thread, subprocessamento e multiprocessamento, que parecem fornecer funcionalidades relacionadas.

Alguma opinião sobre isso? Eu gostaria da solução mais simples e portátil.

Vaibhav Mule
fonte
Relacionado: stackoverflow.com/questions/1743293/… (leia minha resposta lá para ver por que os encadeamentos não são um iniciante para código Python puro)
1
"Qualquer versão do Python" é MUITO vago. Python 2.3? 1.x? 3.x? É simplesmente uma condição impossível de satisfazer.
detly

Respostas:

64

multiprocessingé um ótimo tipo de módulo de canivete suíço. É mais geral do que threads, pois você pode até mesmo realizar cálculos remotos. Este é, portanto, o módulo que eu sugiro que você use.

O subprocessmódulo também permite que você inicie vários processos, mas achei menos conveniente de usar do que o novo módulo de multiprocessamento.

Threads são notoriamente sutis e, com CPython, muitas vezes você está limitado a um núcleo, com eles (embora, como observado em um dos comentários, o Global Interpreter Lock (GIL) possa ser lançado em código C chamado de código Python) .

Eu acredito que a maioria das funções dos três módulos que você cita podem ser usadas de forma independente da plataforma. No lado da portabilidade, observe que multiprocessingsó vem como padrão desde o Python 2.6 (no entanto, existe uma versão para algumas versões mais antigas do Python). Mas é um ótimo módulo!

Eric O Lebigot
fonte
1
para uma atribuição, usei apenas o módulo "multiprocessing" e seu método pool.map (). Pedaco de bolo !
kmonsoor
Também está em consideração algo como o aipo? Por que é ou não é?
user3245268
Pelo que eu posso dizer, Celery está mais envolvido (você precisa instalar algum agente de mensagens), mas é uma opção que provavelmente deve ser considerada, dependendo do problema em questão.
Eric O Lebigot,
186

Para mim, isso é bem simples:

A opção de subprocesso :

subprocessé para executar outros executáveis --- é basicamente um wrapper os.fork()e os.execve()com algum suporte para encanamento opcional (configuração de PIPEs de e para os subprocessos. Obviamente, você poderia outros mecanismos de comunicação entre processos (IPC), como soquetes, ou Posix ou Memória compartilhada SysV. Mas você ficará limitado a quaisquer interfaces e canais IPC suportados pelos programas que você está chamando.

Normalmente, alguém usa qualquer um de forma subprocesssíncrona --- simplesmente chamando algum utilitário externo e lendo sua saída ou aguardando sua conclusão (talvez lendo seus resultados de um arquivo temporário ou depois de publicá-los em algum banco de dados).

No entanto, pode-se gerar centenas de subprocessos e pesquisá-los. Minha própria classe de utilitários favorita faz exatamente isso. A maior desvantagem do subprocessmódulo é que o suporte de E / S geralmente bloqueia. Existe um rascunho do PEP-3145 para consertar isso em alguma versão futura do Python 3.x e um asyncproc alternativo (Aviso que leva direto ao download, não a qualquer tipo de documentação nem README). Eu também descobri que é relativamente fácil apenas importar fcntle manipular seus Popendescritores de arquivo PIPE diretamente - embora eu não saiba se isso é portátil para plataformas não UNIX.

(Atualização: 7 de agosto de 2019: suporte Python 3 para subprocessos ayncio : subprocessos asyncio )

subprocess quase não tem suporte para manipulação de eventos ... embora você possa usar o signalmódulo e sinais simples do UNIX / Linux da velha escola --- matando seus processos suavemente, por assim dizer.

A opção de multiprocessamento :

multiprocessingé para executar funções dentro de seu código (Python) existente com suporte para comunicações mais flexíveis entre esta família de processos. Em particular, é melhor construir seu multiprocessingIPC em torno dos Queueobjetos do módulo onde possível, mas você também pode usar Eventobjetos e vários outros recursos (alguns dos quais são, presumivelmente, criados em torno do mmapsuporte nas plataformas onde esse suporte é suficiente).

O multiprocessingmódulo do Python se destina a fornecer interfaces e recursos que são muito semelhantes threading , permitindo ao CPython escalar seu processamento entre várias CPUs / núcleos, apesar do GIL (Global Interpreter Lock). Ele aproveita todo o bloqueio de SMP minucioso e esforço de coerência que foi feito pelos desenvolvedores do kernel do seu sistema operacional.

A opção de threading :

threadingé para uma gama bastante estreita de aplicativos que são limitados por E / S (não precisam ser escalados em vários núcleos de CPU) e que se beneficiam da latência extremamente baixa e sobrecarga de comutação de troca de thread (com memória de núcleo compartilhada) vs. processo / mudança de contexto. No Linux, este é quase o conjunto vazio (os tempos de troca de processos do Linux são extremamente próximos aos de suas trocas de thread).

threadingsofre de duas desvantagens principais em Python .

Um, é claro, é específico da implementação --- afetando principalmente o CPython. Esse é o GIL. Para a maior parte, a maioria dos programas CPython não se beneficiará da disponibilidade de mais de duas CPUs (núcleos) e frequentemente o desempenho sofrerá com a contenção de bloqueio GIL.

O maior problema, que não é específico da implementação, é que os threads compartilham a mesma memória, manipuladores de sinal, descritores de arquivo e certos outros recursos do sistema operacional. Portanto, o programador deve ser extremamente cuidadoso com o bloqueio de objetos, tratamento de exceções e outros aspectos de seu código que são sutis e podem matar, paralisar ou bloquear todo o processo (conjunto de threads).

Em comparação, o multiprocessingmodelo dá a cada processo sua própria memória, descritores de arquivo, etc. Uma falha ou exceção não tratada em qualquer um deles só matará esse recurso e lidar de forma robusta com o desaparecimento de um filho ou processo irmão pode ser consideravelmente mais fácil do que depurar, isolar e correção ou solução de problemas semelhantes em threads.

  • (Observação: o uso de threadingcom os principais sistemas Python, como NumPy , pode sofrer consideravelmente menos com a contenção de GIL do que a maioria de seu próprio código Python. Isso porque eles foram especificamente projetados para isso; as partes nativas / binárias de NumPy, por exemplo, irá liberar o GIL quando for seguro).

A opção distorcida :

Também é importante notar que Twisted oferece outra alternativa que é elegante e muito difícil de entender . Basicamente, correndo o risco de simplificar demais a ponto de os fãs do Twisted invadirem minha casa com forcados e tochas, o Twisted oferece multitarefa cooperativa orientada a eventos em qualquer processo (único).

Para entender como isso é possível, deve-se ler sobre os recursos do select()(que podem ser construídos em torno de select () ou poll () ou chamadas de sistema de sistema operacional semelhantes). Basicamente, é tudo impulsionado pela capacidade de fazer uma solicitação ao sistema operacional para hibernar enquanto se espera qualquer atividade em uma lista de descritores de arquivo ou algum tempo limite.

O despertar de cada uma dessas chamadas para select()é um evento --- seja envolvendo entrada disponível (legível) em algum número de sockets ou descritores de arquivo, ou espaço de buffer tornando-se disponível em alguns outros descritores ou sockets (graváveis), algumas condições excepcionais (TCP pacotes PUSH fora da banda, por exemplo) ou um TIMEOUT.

Assim, o modelo de programação Twisted é construído em torno do tratamento desses eventos, em seguida, em loop no manipulador "principal" resultante, permitindo que ele despache os eventos para seus manipuladores.

Pessoalmente, penso no nome Twisted como uma evocação do modelo de programação ... já que sua abordagem ao problema deve ser, em certo sentido, "torcida" de dentro para fora. Em vez de conceber seu programa como uma série de operações em dados de entrada e saídas ou resultados, você está escrevendo seu programa como um serviço ou daemon e definindo como ele reage a vários eventos. (Na verdade, o "loop principal" central de um programa Twisted é (normalmente? Sempre?) A reactor()).

Os principais desafios de usar o Twisted envolvem torcer sua mente em torno do modelo orientado a eventos e também evitar o uso de quaisquer bibliotecas de classe ou kits de ferramentas que não foram escritos para cooperar dentro da estrutura Twisted. É por isso que Twisted fornece seus próprios módulos para manipulação de protocolo SSH, para curses, e seu próprio subprocesso / funções Popen, e muitos outros módulos e manipuladores de protocolo que, à primeira vista, parecem duplicar as coisas nas bibliotecas padrão do Python.

Acho que é útil entender o Twisted em um nível conceitual, mesmo que você nunca pretenda usá-lo. Ele pode fornecer insights sobre desempenho, contenção e manipulação de eventos em seu threading, multiprocessamento e até mesmo manipulação de subprocessos, bem como qualquer processamento distribuído que você empreenda.

( Nota: Novas versões do Python 3.x são incluindo asyncio (Asynchronous I / O) apresenta, como assíncrono def , o @ async.coroutine decorador, e aguardam palavra-chave, e rendimento de futuro apoio Todos estes são mais ou menos semelhante ao. Torcido de uma perspectiva de processo (multitarefa cooperativa). (Para obter o status atual do suporte Twisted para Python 3, verifique: https://twistedmatrix.com/documents/current/core/howto/python3.html )

A opção distribuída :

Ainda outro domínio de processamento que você não perguntou, mas que vale a pena considerar, é o de processamento distribuído . Existem muitas ferramentas e estruturas Python para processamento distribuído e computação paralela. Pessoalmente, acho que o mais fácil de usar é aquele que é considerado menos frequentemente naquele espaço.

É quase trivial criar processamento distribuído no Redis . Todo o armazenamento de chaves pode ser usado para armazenar unidades de trabalho e resultados, LISTs do Redis podem ser usados Queue()como objetos semelhantes e o suporte PUB / SUB pode ser usado para Eventmanuseio semelhante. Você pode fazer o hash de suas chaves e valores de uso, replicados em um cluster frouxo de instâncias do Redis, para armazenar a topologia e os mapeamentos de token hash para fornecer hash consistente e failover para escalar além da capacidade de qualquer instância única para coordenar seus trabalhadores e dados de empacotamento (em conserva, JSON, BSON ou YAML) entre eles.

É claro que, à medida que você começa a construir uma solução em maior escala e mais sofisticada em torno do Redis, você está reimplementando muitos dos recursos que já foram resolvidos usando Celery , Apache Spark e Hadoop , Zookeeper , etcd , Cassandra e assim por diante. Todos eles têm módulos para acesso Python a seus serviços.

[Atualização: Alguns recursos a serem considerados se você estiver considerando Python para uso intensivo de computação em sistemas distribuídos: IPython Parallel e PySpark . Embora sejam sistemas de computação distribuída de propósito geral, eles são particularmente acessíveis e populares para a ciência e análise de dados de subsistemas].

Conclusão

Lá você tem uma gama de alternativas de processamento para Python, de single threaded, com chamadas síncronas simples para subprocessos, pools de subprocessos pesquisados, threaded e multiprocessamento, multitarefa cooperativa orientada por evento e processamento externo para distribuído.

Jim Dennis
fonte
1
É difícil usar multiprocessamento com classes / OOP.
Tjorriemorrie
2
@Tjorriemorrie: Imagino que você queira dizer que é difícil despachar chamadas de método para instâncias de objetos que podem estar em outros processos. Eu sugeriria que este é o mesmo problema que você teria com tópicos, mas mais facilmente visível (ao invés de ser frágil e sujeito a condições de corrida obscuras). Eu acho que a abordagem recomendada seria providenciar para que todo esse despacho ocorra por meio de objetos Queue, que funcionam em um único thread, multi-thread e entre processos. (Com alguma implementação de Redis ou Celery Queue, mesmo em um cluster de nós)
Jim Dennis
2
Esta é uma resposta muito boa. Eu gostaria que fosse na introdução à simultaneidade nos documentos do Python3.
root-11 de
1
@ root-11 você pode sugerir isso aos mantenedores do documento; Publiquei aqui para uso livre. Você e eles podem usá-lo, inteiro ou em partes.
Jim Dennis
"Para mim, isso é bem simples:" Adorei. muito obrigado
jerome
5

Em um caso semelhante, optei por processos separados e o pouco de comunicação necessária através de soquete de rede. É altamente portátil e bastante simples de fazer usando python, mas provavelmente não o mais simples (no meu caso, eu também tinha outra restrição: a comunicação com outros processos escritos em C ++).

No seu caso, eu provavelmente optaria por multiprocessos, já que threads de python, pelo menos ao usar CPython, não são threads reais. Bem, eles são threads nativos do sistema, mas os módulos C chamados de Python podem ou não liberar o GIL e permitir que outros threads sejam executados ao chamar o código de bloqueio.

Kriss
fonte
4

Para usar vários processadores em CPython, sua única opção é o multiprocessingmódulo. CPython mantém um bloqueio em seus internos (o GIL ), o que impede que threads em outros cpus funcionem em paralelo. O multiprocessingmódulo cria novos processos (como subprocess) e gerencia a comunicação entre eles.

Jochen Ritzel
fonte
5
Isso não é bem verdade, AFAIK você pode liberar o GIL usando a API C, e existem outras implementações de Python, como IronPython ou Jython, que não sofrem com essas limitações. Eu não votei negativamente, no entanto.
Bastien Léonard
1

Desembarque e deixe o unix fazer seu trabalho:

use iterpipes para envolver o subprocesso e, em seguida:

Do site de Ted Ziuba

INPUTS_FROM_YOU | xargs -n1 -0 -P NUM ./process #NUM processos paralelos

OU

Gnu Parallel também servirá

Você sai com GIL enquanto manda os meninos dos bastidores para fazer seu trabalho multicore.

chiggsy
fonte
6
"Portabilidade é importante, pois gostaria que rodasse em qualquer versão do Python no Mac, Linux e Windows."
detly
Com esta solução, você pode interagir repetidamente com o trabalho? Você pode fazer isso em multiprocessamento, mas não acho que em subprocesso.
abalter