Estou aprendendo como usar o threading
e os multiprocessing
módulos em Python para executar certas operações em paralelo e acelerar o meu código.
Estou achando isso difícil (talvez porque eu não tenha nenhuma base teórica sobre isso) entender qual é a diferença entre um threading.Thread()
objeto e multiprocessing.Process()
um.
Além disso, não está totalmente claro para mim como instanciar uma fila de trabalhos e ter apenas 4 (por exemplo) deles executando paralelamente, enquanto o outro aguarda a liberação dos recursos antes de serem executados.
Acho os exemplos claros na documentação, mas não muito exaustivos; Assim que tento complicar um pouco as coisas, recebo muitos erros estranhos (como um método que não pode ser usado em conserva e assim por diante).
Então, quando devo usar os módulos threading
e multiprocessing
?
Você pode me vincular a alguns recursos que explicam os conceitos por trás desses dois módulos e como usá-los adequadamente para tarefas complexas?
Thread
módulo (chamado_thread
em python 3.x). Para ser honesto, eu nunca entendi as diferenças mim ...Thread
/_thread
diz explicitamente, são "primitivas de baixo nível". Você pode usá-lo para objetos de sincronização de compilação personalizada, para controlar a ordem de uma árvore de tópicos, etc. participação Se você não pode imaginar por que você precisa usá-lo, não usá-lo, e ficar comthreading
.Respostas:
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: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:
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 emmultiprocessing.Process
vez de confiar em piscinas. E você terá que criarqueue.Queue
oumultiprocessing.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:
ctypes
tipos.threading
módulo não possui alguns dos recursos domultiprocessing
módulo. (Você pode usarmultiprocessing.dummy
para obter a maior parte da API ausente sobre os encadeamentos, ou pode usar módulos de nível superior comoconcurrent.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.
fonte
pickle
docs explicá-lo), e, em seguida, na pior das hipóteses o seu invólucro estúpido édef wrapper(obj, *args): return obj.wrapper(*args)
.Vários encadeamentos podem existir em um único processo. Os encadeamentos que pertencem ao mesmo processo compartilham a mesma área de memória (podem ler e gravar nas mesmas variáveis e podem interferir entre si). Pelo contrário, diferentes processos vivem em diferentes áreas da memória e cada um deles tem suas próprias variáveis. Para se comunicar, os processos precisam usar outros canais (arquivos, tubos ou soquetes).
Se você deseja paralelizar uma computação, provavelmente precisará de multithreading, porque provavelmente deseja que os threads cooperem na mesma memória.
Falando sobre desempenho, os threads são mais rápidos de criar e gerenciar do que processos (porque o sistema operacional não precisa alocar uma área de memória virtual totalmente nova), e a comunicação entre threads geralmente é mais rápida que a comunicação entre processos. Mas os threads são mais difíceis de programar. Os encadeamentos podem interferir uns com os outros e podem gravar na memória um do outro, mas a maneira como isso acontece nem sempre é óbvia (devido a vários fatores, principalmente a reordenação de instruções e o cache de memória) e, portanto, você precisará de primitivas de sincronização para controlar o acesso para suas variáveis.
fonte
threading
emultiprocessing
.Acredito que este link responda à sua pergunta de maneira elegante.
Para ser breve, se um de seus subproblemas tiver que esperar enquanto outro termina, o multithreading é bom (em operações pesadas de E / S, por exemplo); por outro lado, se seus subproblemas realmente acontecerem ao mesmo tempo, o multiprocessamento é sugerido. No entanto, você não criará mais processos do que seu número de núcleos.
fonte
Citações da documentação do Python
Eu destaquei as principais citações da documentação do Python sobre Process vs Threads e o GIL em: O que é o bloqueio global de intérprete (GIL) no CPython?
Experiências de processo versus encadeamento
Fiz um pouco de benchmarking para mostrar a diferença mais concretamente.
No benchmark, cronometrei o trabalho vinculado da CPU e da E / S para vários números de threads em uma CPU com 8 hyperthread . O trabalho fornecido por rosca é sempre o mesmo, de modo que mais roscas signifiquem mais trabalho total fornecido.
Os resultados foram:
Plotar dados .
Conclusões:
Para o trabalho vinculado à CPU, o multiprocessamento é sempre mais rápido, provavelmente devido ao GIL
para trabalho vinculado de E / S. ambos são exatamente a mesma velocidade
os encadeamentos escalam apenas cerca de 4x em vez dos 8x esperados, pois estou em uma máquina com 8 hyperthread
Compare isso com um trabalho vinculado à CPU C POSIX que atinja a aceleração de 8x esperada: o que 'real', 'user' e 'sys' significam na saída do tempo (1)?
TODO: Não sei o motivo disso, deve haver outras ineficiências do Python entrando em jogo.
Código do teste:
Upstream do GitHub + código de plotagem no mesmo diretório .
Testado no Ubuntu 18.10, Python 3.6.7, em um laptop Lenovo ThinkPad P51 com CPU: CPU Intel Core i7-7820HQ (4 núcleos / 8 threads), RAM: 2x Samsung M471A2K43BB1-CRC (2x 16GiB), SSD: Samsung MZVLB512HAJQ- 000L7 (3.000 MB / s).
Visualize quais threads estão sendo executados em um determinado momento
Este post https://rohanvarma.me/GIL/ me ensinou que você pode executar um retorno de chamada sempre que um thread for agendado com o
target=
argumentothreading.Thread
e o mesmo paramultiprocessing.Process
.Isso nos permite ver exatamente qual thread é executado a cada momento. Quando isso for feito, veremos algo como (criei este gráfico específico):
o que mostraria que:
fonte
Aqui estão alguns dados de desempenho do python 2.6.x que questionam a noção de que o encadeamento é mais eficiente que o multiprocessamento em cenários vinculados à IO. Esses resultados são de um IBM System x3650 M4 BD de 40 processadores.
Processamento vinculado à IO: o conjunto de processos teve um desempenho melhor que o conjunto de threads
Processamento vinculado à CPU: o conjunto de processos teve um desempenho melhor que o conjunto de threads
Esses não são testes rigorosos, mas eles me dizem que o multiprocessamento não é totalmente ineficaz em comparação com o encadeamento.
Código usado no console python interativo para os testes acima
fonte
>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms
>>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms
>>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms
>>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Bem, a maior parte da pergunta é respondida por Giulio Franco. Vou aprofundar o problema do consumidor-produtor, que suponho que o colocará no caminho certo para a sua solução usar um aplicativo multithread.
Você pode ler mais sobre as primitivas de sincronização em:
O pseudocódigo está acima. Suponho que você deva procurar no problema produtor-consumidor para obter mais referências.
fonte