curso
Multiprocessamento Python: Um guia para threads e processos
A biblioteca padrão do Python vem equipada com vários pacotes integrados para que os desenvolvedores comecem a aproveitar os benefícios da linguagem instantaneamente.
Um desses pacotes é o módulo de multiprocessamento, que permite que os sistemas executem vários processos simultaneamente. Em outras palavras, os desenvolvedores podem dividir os aplicativos em threads menores que podem ser executados independentemente do código Python. Esses threads ou processos são então alocados no processador pelo sistema operacional, permitindo que sejam executados em paralelo, melhorando o desempenho e a eficiência dos seus programas Python.
Se palavras como threads, processos, processadores etc. não forem familiares para você, não se preocupe. Neste artigo, abordaremos a definição de um processo, como eles diferem dos threads e como usar o módulo multiprocessing
.
O que são processos em Python?
Entender o conceito de processos e threads é extremamente útil para compreender melhor como um sistema operacional gerencia os programas nos vários estágios de execução. Um processo é apenas uma referência a um programa de computador. Cada programa tem um processo associado a ele.
Enquanto você lê este artigo, é muito provável que seu computador tenha muitos processos em execução ao mesmo tempo, mesmo que você tenha apenas alguns programas abertos - isso se deve ao fato de a maioria dos sistemas operacionais ter várias tarefas em execução em segundo plano. Por exemplo, você pode ter apenas três programas em execução neste momento, mas seu computador pode ter mais de 30 processos ativos em execução simultaneamente.
A maneira de verificar os processos ativos em execução no computador depende do seu sistema operacional:
- No Windows: Ctrl+Shift+Esc iniciará o gerenciador de tarefas
- No Mac: abra a pesquisa do Spotlight no Mac e digite "Activity Monitor", depois pressione Return
- No Linux: clique no menu Aplicativo e procure o System Monitor.
O Python fornece acesso a processos reais em nível de sistema. A instanciação de uma instância da classe Process
do módulo de multiprocessamento permite que os desenvolvedores façam referência ao processo nativo subjacente usando Python. Um novo processo nativo é criado nos bastidores quando um processo é iniciado. O ciclo de vida de um processo Python consiste em três estágios: O início de um novo processo, o processo em execução e o processo encerrado - abordaremos cada estágio.
Todos os processos são compostos por um ou mais threads. Você se lembra que mencionamos que "um processo é apenas uma referência a um programa de computador"? Bem, cada programa Python é um processo que consiste em um thread padrão chamado thread principal. O thread principal é responsável por executar as instruções nos programas Python. No entanto, é importante observar que os processos e os threads são diferentes.
Multiprocessamento vs. Rosqueamento
Para ser mais específico, uma instância do interpretador do Python - a ferramenta que converte o código escrito em Python para a linguagem que um computador pode entender - é igual a um processo. Um processo consistirá em pelo menos um thread, chamado de "thread principal" em Python, embora outros threads possam ser criados no mesmo processo - todos os outros threads criados em um processo pertencerão a esse processo.
O thread serve como uma representação de como seu programa Python será executado e, quando todos os threads que não são de segundo plano forem encerrados, o processo Python será encerrado.
- Processo: Um processo é uma instância do interpretador Python que consiste em pelo menos um thread chamado thread principal.
- Tópico: Uma representação de como um programa Python é executado em um processo Python.
O Python tem duas classes extremamente semelhantes que nos dão mais controle sobre processos e threads: multiprocessing.Process e threading.Thread.
Vamos analisar algumas de suas semelhanças e diferenças.
Semelhanças
Concorrência
A simultaneidade é um conceito no qual diferentes partes do programa podem ser executadas fora de ordem ou em ordem parcial sem que o resultado final seja afetado. Ambas as classes foram inicialmente planejadas para simultaneidade.
Suporte a primitivos de concorrência
O multiprocessamento, o processo e o encadeamento. As classes de thread suportam as mesmas primitivas de simultaneidade - uma ferramenta que permite a sincronização e a coordenação de threads e processos.
API uniforme
Quando você tiver entendido o multiprocessamento. Process, então você pode transferir esse conhecimento para a API threading.Thread e vice-versa. Eles foram projetados intencionalmente dessa forma.
Diferenças
Funcionalidade
Apesar de suas APIs serem as mesmas, os processos e os threads são diferentes. Um processo é um nível mais alto de abstração do que um thread: um processo é uma referência a um programa de computador, e um thread pertence a um processo. Essa diferença é inerente às classes. Assim, as classes representam duas funções nativas diferentes gerenciadas por um sistema operacional subjacente.
Acesso ao estado compartilhado
As duas classes acessam o estado compartilhado de forma diferente. Como os threads pertencem a um processo, eles podem compartilhar memória em um processo. Assim, uma função executada em um novo thread ainda tem acesso aos mesmos dados e ao mesmo estado em um processo. O modo como os threads compartilham estados entre si é conhecido como "memória compartilhada" e é bastante simples. Por outro lado, o compartilhamento de estados entre processos é muito mais complexo: o estado deve ser serializado e transmitido entre os processos. Em outras palavras, os processos não usam a memória compartilhada para compartilhar estados porque têm memória separada. Em vez disso, os processos são compartilhados usando uma técnica chamada "comunicação entre processos" e, para executá-la no Python, são necessárias outras ferramentas explícitas, como multiprocessing.Pipe ou multiprocessing.Queue.
GIL
O GIL (Global Interpreter Lock) do Python é um bloqueio que permite que apenas um thread mantenha o controle sobre o interpretador Python. Vários threads estão sujeitos ao GIL, o que geralmente torna o uso do Python para executar multithreading uma má ideia: a verdadeira execução de vários núcleos por meio de multithreading não é suportada pelo Python no interpretador CPython. No entanto, os processos não estão sujeitos ao GIL porque o GIL é usado em cada processo Python, mas não em todos os processos.
Em termos de casos de uso, o multiprocessamento geralmente supera o threading em cenários em que o programa consome muita CPU e não precisa executar nenhuma E/S ou interação com o usuário. O threading é a melhor solução para programas que são vinculados à E/S ou à rede e em cenários em que o objetivo é tornar o aplicativo mais responsivo.
Os benefícios do multiprocessamento do Python
Pense em um processador como um empreendedor. À medida que o negócio do empreendedor cresce, há mais tarefas que precisam ser gerenciadas para acompanhar o crescimento do negócio. Se a empreendedora decidir assumir todas essas tarefas sozinha (ou seja, contabilidade, vendas, marketing, inovação etc.), ela corre o risco de prejudicar a eficiência e o desempenho geral da empresa, pois uma pessoa só pode fazer uma quantidade limitada de coisas ao mesmo tempo. Por exemplo, antes de passar para as tarefas de inovação, ela deve interromper as tarefas de vendas - isso é conhecido como executar tarefas "sequencialmente".
A maioria dos empreendedores entende que tentar fazer tudo sozinho é uma má ideia. Consequentemente, elas normalmente compensam o número crescente de tarefas contratando funcionários para gerenciar vários departamentos. Dessa forma, as tarefas podem ser executadas em paralelo, o que significa que uma tarefa não precisa ser interrompida para que outra seja executada. A contratação de mais funcionários para realizar tarefas específicas é semelhante ao uso de vários processadores para realizar operações. Por exemplo, os projetos de visão computacional são bastante exigentes, pois normalmente você precisa processar muitos dados de imagem, o que consome muito tempo: para acelerar esse procedimento, você pode processar várias imagens em paralelo.
Assim, podemos dizer que o multiprocessamento é útil para tornar os programas mais eficientes, dividindo e atribuindo tarefas a diferentes processadores. O módulo de multiprocessamento do Python simplifica isso ainda mais, servindo como uma ferramenta de alto nível para aumentar a eficiência dos seus programas, atribuindo tarefas a diferentes processos.
Noções básicas sobre o módulo de multiprocessamento do Python
Mencionamos que o ciclo de vida de um processo Python consiste em três estágios: o novo processo, o processo em execução e o processo encerrado. Esta seção se aprofundará em cada fase do ciclo de vida e fornecerá exemplos codificados. Você pode acessar o código para acompanhar o processo acessando nossa pasta de trabalho do DataLab.
O novo processo
Um novo processo pode ser definido como o processo que foi criado ao instanciar uma instância da classe Process
. Um processo filho é gerado quando atribuímos o objeto Process
a uma variável.
from multiprocessing import Process
# Create a new process
process = Process()
No momento, nossa instância de processo não está fazendo nada porque inicializamos um objeto Process vazio. Podemos alterar as configurações do nosso objeto Process para executar uma função específica, passando uma função que queremos executar em um processo diferente para o parâmetro de destino da classe.
# Create a new process with a specified function to execute.
def example_function():
pass
new_process = Process(target=example_function)
Se a nossa função de destino também tivesse parâmetros, nós simplesmente os passaríamos para o parâmetro args do objeto Process como uma tupla.
Dica: Aprenda a escrever funções em Python com o curso interativo Writing Functions in Python.
# Create a new process with specific function to execute with args.
def example(args):
pass
process = Process(target=example, args=("Hi",))
Observe que criamos apenas um novo processo, mas ele ainda não está em execução.
Vamos ver como podemos executar um novo processo.
O processo em execução
A execução de um novo processo é bastante simples: basta chamar o método start()
da instância do processo.
# Run the new process
process.start()
Essa ação inicia a atividade do processo chamando o método run()
da instância do processo sob o capô. O método run()
também é responsável por chamar a função personalizada especificada no parâmetro de destino da instância do processo (se especificado).
Lembre-se de que, no início do tutorial, afirmamos que cada processo tem pelo menos um thread chamado thread principal - esse é o padrão. Assim, quando um processo filho é iniciado, a thread principal é criada para esse processo filho e é iniciada. O thread principal é responsável pela execução de todo o nosso código no processo filho.
Podemos verificar se nossa instância de processo está ativa desde o retorno do método start()
até o encerramento do processo filho usando o método is_alive()
em nossa instância Process
.
Observação: Se você estiver testando isso em um notebook, a chamada para is_alive()
deverá estar na mesma célula que a chamada para o método start()
para que ele capture o processo em execução.
# Run the new process
process.start()
# Check process is running
process.is_alive()
"""
True
"""
Se o processo não estivesse em execução, a chamada para o método is_alive()
retornaria False.
O processo encerrado
Quando a função run()
retorna ou sai, o processo é encerrado: não precisamos necessariamente fazer nada explicitamente. Em outras palavras, você pode esperar que um processo seja encerrado após a conclusão das instruções que você definiu como função de destino. Portanto, o método is_alive()
retornaria falso.
# Check process is terminated - should return false.
process.is_alive()
"""
False
"""
No entanto, um processo também pode ser encerrado se encontrar uma exceção não tratada ou se for gerado um erro. Por exemplo, se houver um erro na função que você definiu como alvo, o processo será encerrado.
# Create a new process with a specific function that has an error
# to execute with args.
def example(args):
split_args = list(args.split())
# "name" variable is not in the function namespace - should raise error
return name
# New process
process = Process(target=example, args=("Hi",))
# Running the new process
process.start()
# Check process is running
process.is_alive()
"""
True
Process Process-15:
Traceback (most recent call last):
File "/usr/lib/python3.8/multiprocessing/process.py", line 315, in _bootstrap
self.run()
"""
O objeto Process também tem os métodos terminate()
e kill()
que permitem aos usuários encerrar um processo de forma forçada.
# Create a new process with a specific function to execute with args.
def example(args):
split_args = list(args.split())
# "name" variable is not in the function namespace - should raise error
return split_args
# New process
process = Process(target=example, args=("Hi",))
# Running the new process
process.start()
if process.is_alive():
process.terminate() # You can also use process.kill()
print("Process terminated forcefully")
"""
Process terminated forcefully
"""
É importante observar que os encerramentos forçados não são a maneira recomendada de encerrar um processo: eles podem não fechar todos os recursos abertos com segurança ou armazenar o estado necessário do programa. Uma solução melhor é usar um desligamento controlado com um sinalizador booleano seguro para o processo ou uma ferramenta semelhante.
Tutorial de multiprocessamento do Python
Agora que você entende os conceitos básicos de multiprocessamento, vamos trabalhar em um exemplo para demonstrar como fazer programação simultânea em Python.
A função que criamos simplesmente imprimirá uma declaração, dormirá por 1 segundo e, em seguida, imprimirá outro sleep - saiba mais sobre funções neste tutorial sobre funções do Python.
import time
def do_something():
print("I'm going to sleep")
time.sleep(1)
print("I'm awake")
A primeira etapa é criar um novo processo: vamos criar dois.
# Create new child process
process_1 = Process(target=do_something)
process_2 = Process(target=do_something)
Veja como um programa simultâneo será exibido em um ambiente de notebook:
%%time
# Starts both processes
process_1.start()
process_2.start()
"""
I'm going to sleep
CPU times: user 810 µs, sys: 7.34 ms, total: 8.15 ms
Wall time: 6.04 ms
I'm going to sleep
"""
Considerando o resultado do programa, é bastante evidente que há um problema em algum ponto do nosso código. O timer é impresso no meio do nosso primeiro processo, e a segunda instrução de impressão não é impressa.
Isso ocorre porque há três processos em execução: o processo principal, process_1
e process_2
. O processo que rastreia o tempo e o imprime é o processo principal. Para que nosso processo principal espere antes de imprimir a hora, devemos chamar o método join()
em nossos dois processos depois de executá-los.
Observação: Se você estiver interessado em saber mais, confira esta discussão no Stackoverflow.
Vamos dar uma olhada em nosso novo trecho de código:
%%time
# Create new child process (Cannot run a process more than once)
new_process_1 = Process(target=do_something)
new_process_2 = Process(target=do_something)
# Starts both processes
new_process_1.start()
new_process_2.start()
new_process_1.join()
new_process_2.join()
"""
I'm going to sleep
I'm going to sleep
I'm awake
I'm awake
CPU times: user 0 ns, sys: 14 ms, total: 14 ms
Wall time: 1.01 s
"""
Problema resolvido.
O tempo de parede dessa corrida foi um pouco maior do que o da primeira corrida. No entanto, essa execução concluiu as chamadas para as duas funções de destino antes de retornar as informações de tempo. Também podemos aplicar esse mesmo raciocínio para fazer com que mais processos sejam executados simultaneamente.
Considerações finais sobre o multiprocessamento do Python
Neste tutorial, você aprendeu como tornar os programas Python mais eficientes executando-os simultaneamente. Especificamente, você aprendeu:
- O que são processos e como você pode visualizá-los em seu computador.
- As semelhanças e diferenças entre os módulos de multiprocessamento e de threading do Python.
- Os conceitos básicos do módulo de multiprocessamento e como executar um programa Python simultaneamente usando o multiprocessamento.
Você quer saber mais sobre programação em Python? Confira nosso plano de carreira para programador Python. Não é necessário ter experiência prévia em programação e, ao final do curso, você terá adquirido as habilidades necessárias para desenvolver software, manipular dados e realizar análises avançadas em Python.
Torne-se um desenvolvedor Python
Cursos para Python
curso
Introduction to Data Science in Python
curso
Visualizing Time Series Data in Python
blog
6 práticas recomendadas de Python para um código melhor
blog
5 desafios Python para desenvolver suas habilidades
DataCamp Team
5 min
tutorial
Otimização em Python: Técnicas, pacotes e práticas recomendadas
tutorial
21 ferramentas essenciais do Python
tutorial
Tutorial de manipulação de dados categóricos de aprendizado de máquina com Python
tutorial
Desenvolvimento de back-end em Python: Um guia completo para iniciantes
Oluseye Jeremiah
26 min