O que Giulio Franco diz é verdadeiro para multithreading vs. multiprocessing em geral .
No entanto, o Python * tem um problema adicional: existe um bloqueio global para intérpretes que impede que dois threads no mesmo processo executem o código Python ao mesmo tempo. Isso significa que se você tiver 8 núcleos e alterar seu código para usar 8 threads, ele não poderá usar 800% da CPU e executar 8x mais rapidamente; ele usará a mesma CPU 100% e será executado na mesma velocidade. (Na realidade, o processo será um pouco mais lento, porque há sobrecarga extra na segmentação, mesmo que você não tenha nenhum dado compartilhado, mas ignore-o por enquanto.)
Há exceções para isto. Se a computação pesada do seu código não acontecer de fato no Python, mas em alguma biblioteca com código C personalizado que manipula corretamente o GIL, como um aplicativo numpy, você obterá o benefício esperado do desempenho da segmentação. O mesmo acontece se a computação pesada for feita por algum subprocesso que você executa e espera.
Mais importante, há casos em que isso não importa. Por exemplo, um servidor de rede passa a maior parte do tempo lendo pacotes fora da rede, e um aplicativo GUI passa a maior parte do tempo aguardando eventos do usuário. Um motivo para usar threads em um servidor de rede ou aplicativo GUI é permitir que você execute "tarefas em segundo plano" de longa execução sem impedir que o thread principal continue a atender pacotes de rede ou eventos da GUI. E isso funciona muito bem com threads Python. (Em termos técnicos, isso significa que os threads do Python oferecem concorrência, mesmo que não ofereçam paralelismo central.)
Mas se você estiver escrevendo um programa vinculado à CPU em Python puro, o uso de mais threads geralmente não é útil.
O uso de processos separados não apresenta problemas com o GIL, porque cada processo possui seu próprio GIL separado. É claro que você ainda tem as mesmas vantagens entre threads e processos que em qualquer outro idioma - é mais difícil e mais caro compartilhar dados entre processos do que entre threads, pode ser caro executar um grande número de processos ou criar e destruir frequentemente, etc. Mas o GIL pesa muito na balança em relação aos processos, de uma maneira que não é verdadeira para, digamos, C ou Java. Portanto, você se encontrará usando multiprocessamento com muito mais frequência em Python do que em C ou Java.
Enquanto isso, a filosofia "baterias incluídas" do Python traz boas notícias: É muito fácil escrever código que pode ser alternado entre threads e processos com uma alteração de uma linha.
Se você projetar seu código em termos de "trabalhos" independentes que não compartilham nada com outros trabalhos (ou o programa principal), exceto entrada e saída, você pode usar a concurrent.futures
biblioteca para escrever seu código em um pool de threads como este:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
Você pode até obter os resultados desses trabalhos e repassá-los para outros trabalhos, aguardar as coisas em ordem de execução ou ordem de conclusão, etc .; leia a seção sobre Future
objetos para obter detalhes.
Agora, se o seu programa estiver constantemente usando 100% da CPU, e adicionar mais threads apenas o tornará mais lento, você estará enfrentando o problema do GIL e precisará mudar para os processos. Tudo o que você precisa fazer é alterar a primeira linha:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
A única ressalva real é que os argumentos e os valores de retorno de seus trabalhos precisam ser selecionáveis (e não levar muito tempo ou memória para separar) para serem utilizados no processo cruzado. Geralmente isso não é um problema, mas às vezes é.
Mas e se seus trabalhos não puderem ser independentes? Se você pode criar seu código em termos de trabalhos que transmitem mensagens de um para outro, ainda é muito fácil. Você pode ter que usar threading.Thread
ou em multiprocessing.Process
vez de confiar em piscinas. E você terá que criar queue.Queue
ou multiprocessing.Queue
objetos explicitamente. (Existem muitas outras opções - tubos, soquetes, arquivos com bandos, ... mas o ponto é que você precisa fazer algo manualmente se a mágica automática de um Executor for insuficiente.)
Mas e se você não puder confiar na passagem de mensagens? E se você precisar de dois trabalhos para alterar a mesma estrutura e ver as mudanças um do outro? Nesse caso, você precisará fazer a sincronização manual (bloqueios, semáforos, condições etc.) e, se desejar usar processos, objetos explícitos de memória compartilhada para inicializar. É quando o multithreading (ou multiprocessing) fica difícil. Se você pode evitá-lo, ótimo; se você não puder, precisará ler mais do que alguém pode colocar em uma resposta do SO.
Em um comentário, você queria saber o que há de diferente entre threads e processos no Python. Realmente, se você ler a resposta de Giulio Franco e a minha e todos os nossos links, isso deverá cobrir tudo ... mas um resumo seria definitivamente útil, então aqui vai:
- Threads compartilham dados por padrão; processos não.
- Como conseqüência de (1), o envio de dados entre processos geralmente requer a decapagem e a remoção da seleção. **
- Como outra consequência de (1), o compartilhamento direto de dados entre processos geralmente exige colocá-los em formatos de baixo nível, como Valor, Matriz e
ctypes
tipos.
- Os processos não estão sujeitos ao GIL.
- Em algumas plataformas (principalmente Windows), os processos são muito mais caros para criar e destruir.
- Existem algumas restrições extras nos processos, algumas das quais são diferentes em plataformas diferentes. Consulte Diretrizes de programação para obter detalhes.
- O
threading
módulo não possui alguns dos recursos do multiprocessing
módulo. (Você pode usar multiprocessing.dummy
para obter a maior parte da API ausente sobre os encadeamentos, ou pode usar módulos de nível superior como concurrent.futures
e não se preocupar com isso.)
* Na verdade, não é o Python, a linguagem, que tem esse problema, mas o CPython, a implementação "padrão" dessa linguagem. Algumas outras implementações não têm um GIL, como o Jython.
** Se você estiver usando o método fork start para multiprocessamento - que é possível na maioria das plataformas que não sejam Windows - cada processo filho obtém os recursos que o pai tinha quando o filho foi iniciado, o que pode ser outra maneira de passar dados para filhos.
Thread
módulo (chamado_thread
em python 3.x). Para ser honesto, eu nunca entendi as diferenças mim ...