Neste guia, você vai aprender a utilizar o módulo de *threading*, que já vem integrado ao Python, para explorar as capacidades de multi-tarefas (multithreading) que o Python oferece.
Iniciando com os conceitos básicos de processos e *threads*, você entenderá como o multithreading funciona no Python, abrangendo também ideias como concorrência e paralelismo. Em seguida, aprenderá a iniciar e executar um ou mais *threads* usando o módulo *threading* que já está incluído no Python.
Vamos começar!
Processos vs. *Threads*: Quais as diferenças?
O que é um processo?
Um processo é qualquer instância de um programa em execução.
Pode ser algo simples como um script Python, um navegador como o Chrome, ou mesmo um aplicativo de videoconferência. Se você abrir o Gerenciador de Tarefas no seu computador e for em Desempenho -> CPU, poderá ver os processos e *threads* que estão ativos nos núcleos da sua CPU.
Entendendo Processos e *Threads*
Internamente, cada processo possui um espaço de memória exclusivo onde são guardados tanto o código quanto os dados necessários para sua execução.
Um processo é composto por um ou mais *threads*. Um *thread* é a menor sequência de instruções que o sistema operacional consegue executar, representando o fluxo de execução.
Cada *thread* tem sua própria pilha e registradores, mas não tem um espaço de memória exclusivo. Todos os *threads* associados a um processo conseguem acessar os mesmos dados. Portanto, dados e memória são compartilhados entre todos os *threads* de um processo.
Em uma CPU com N núcleos, N processos diferentes podem ser executados ao mesmo tempo. No entanto, dois *threads* do mesmo processo jamais podem ser executados de forma paralela, mas podem ser executados simultaneamente. A diferença entre simultaneidade e paralelismo será explicada na próxima seção.
Com base no que aprendemos, vamos resumir as diferenças entre processos e *threads*.
Característica | Processo | *Thread* |
Memória | Memória dedicada | Memória compartilhada |
Modo de execução | Paralelo, concorrente | Concorrente, mas não paralelo |
Execução manipulada por | Sistema Operacional | Interpretador CPython |
Multithreading no Python
No Python, o Global Interpreter Lock (GIL) garante que apenas um *thread* por vez possa adquirir o bloqueio e executar. Todos os *threads* precisam desse bloqueio para serem executados. Isso assegura que apenas um único *thread* esteja em execução em um determinado momento, evitando multithreading simultâneo.
Por exemplo, imagine dois *threads*, t1 e t2, do mesmo processo. Como *threads* compartilham os mesmos dados, enquanto t1 estiver lendo um valor k, t2 poderia modificar esse mesmo valor k. Isso poderia levar a problemas e resultados inesperados. Mas, apenas um *thread* por vez consegue o bloqueio e pode executar. Assim, o GIL também garante segurança para o *thread*.
Então, como alcançamos multithreading no Python? Para entender isso, vamos discutir os conceitos de concorrência e paralelismo.
Concorrência vs. Paralelismo: Uma Visão Geral
Imagine uma CPU com mais de um núcleo. Na imagem abaixo, a CPU tem quatro núcleos. Isso significa que podemos ter quatro operações diferentes executando ao mesmo tempo em cada um dos núcleos.
Se houver quatro processos, cada um pode executar de forma independente e simultânea em um dos quatro núcleos. Suponha que cada processo tenha dois *threads*.
Para entender como *threading* funciona, vamos mudar de uma arquitetura de processador com múltiplos núcleos para uma com apenas um núcleo. Como mencionado, apenas um único *thread* pode estar ativo em um dado instante; mas o núcleo do processador pode alternar entre os *threads*.
Por exemplo, *threads* que lidam com operações de E/S geralmente aguardam por operações como: leitura de entrada do usuário, leitura de dados de um banco de dados e operações com arquivos. Durante esse período de espera, o *thread* pode liberar o bloqueio para que outro *thread* possa executar. O tempo de espera pode também ser uma operação simples como aguardar n segundos.
Em resumo: durante operações de espera, o *thread* libera o bloqueio, permitindo que o núcleo do processador alterne para outro *thread*. O *thread* anterior retoma a execução assim que o período de espera termina. Esse processo, onde o núcleo do processador alterna entre *threads* simultaneamente, torna o multithreading possível. ✅
Se você quer implementar paralelismo no nível de processo no seu aplicativo, considere utilizar multiprocessamento.
Módulo Threading do Python: Primeiros Passos
O Python vem com um módulo de *threading* que pode ser importado em um script Python.
import threading
Para criar um objeto *thread* no Python, você usa o construtor *Thread*: *threading.Thread(…)*. Esta é a sintaxe genérica que funciona bem para grande parte das implementações de *threading*:
threading.Thread(target=...,args=...)
Onde:
- *target* é o argumento que denota a função Python que será executada.
- *args* é uma tupla de argumentos que a função *target* recebe.
Você vai precisar do Python 3.x para executar os códigos de exemplo deste guia. Baixe o código e acompanhe!
Como definir e executar *threads* no Python
Vamos definir um *thread* que executa uma função de destino.
A função de destino é *some_func*.
import threading
import time
def some_func():
print("Executando some_func...")
time.sleep(2)
print("some_func finalizada.")
thread1 = threading.Thread(target=some_func)
thread1.start()
print(threading.active_count())
Vamos analisar o que o código acima faz:
- Importa os módulos *threading* e *time*.
- A função *some_func* tem comandos de print() que explicam o que está acontecendo, e uma operação de espera de dois segundos: *time.sleep(n)* faz com que a função espere por *n* segundos.
- Em seguida, definimos um *thread*, chamado *thread_1*, que tem como alvo a função *some_func*. *threading.Thread(target=…)* cria um objeto *thread*.
- Observação: coloque o nome da função e não a chamada da função. Utilize *some_func* e não *some_func()*.
- A criação de um objeto *thread* não inicia a execução de um *thread*; você precisa chamar o método *start()* para que isso ocorra.
- Para descobrir quantos *threads* estão ativos, usamos a função *active_count()*.
O script Python está sendo executado no *thread* principal e nós estamos criando um outro *thread* (*thread1*) para executar a função *some_func*, então a contagem de *threads* ativas é dois, como vemos na saída:
# Saída
Executando some_func...
2
some_func finalizada.
Se olharmos a saída com atenção, vamos ver que ao iniciar o *thread1*, o primeiro comando print é executado. Mas, durante a operação de espera, o processador alterna para o *thread* principal e imprime o número de *threads* ativos — sem esperar que o *thread1* finalize sua execução.
Esperando que os *Threads* terminem a execução
Se você quiser que o *thread1* termine a execução, você pode chamar o método *join()* depois de iniciar o *thread*. Isso fará com que o código espere que o *thread1* termine a execução, antes de retornar para o *thread* principal.
import threading
import time
def some_func():
print("Executando some_func...")
time.sleep(2)
print("some_func finalizada.")
thread1 = threading.Thread(target=some_func)
thread1.start()
thread1.join()
print(threading.active_count())
Agora, *thread1* vai finalizar a execução antes de imprimirmos a contagem de *threads* ativas. Portanto, apenas o *thread* principal estará em execução, o que significa que a contagem de *threads* ativas é um. ✅
# Saída
Executando some_func...
some_func finalizada.
1
Como executar múltiplos *threads* no Python
Agora, vamos criar dois *threads* para executar duas funções diferentes.
Aqui, *count_down* é uma função que recebe um número como argumento e faz a contagem regressiva desse número até zero.
def count_down(n):
for i in range(n,-1,-1):
print(i)
Vamos definir *count_up*, outra função Python que conta de zero até um determinado número.
def count_up(n):
for i in range(n+1):
print(i)
📑 Quando usamos a função *range()* com a sintaxe *range(start, stop, step)*, o número final é excluído por padrão.
– Para fazer a contagem regressiva de um número específico até zero, você pode usar um valor negativo de *-1* como passo, e o valor final como *-1*, para que o zero seja incluído.
– Do mesmo modo, para contar até *n*, o valor final deve ser *n+1*. Como os valores padrão para início e passo são 0 e 1, você pode usar *range(n + 1)* para obter a sequência de *0* até *n*.
Em seguida, definimos dois *threads*, *thread1* e *thread2*, para executar as funções *count_down* e *count_up*, respectivamente. Adicionamos comandos *print* e operações de espera para ambas as funções.
Ao criar os objetos *thread*, os argumentos para a função devem ser especificados como uma tupla no parâmetro *args*. Como as duas funções ( *count_down* e *count_up*) recebem um argumento, você precisa inserir uma vírgula explicitamente após o valor. Isso garante que o argumento seja passado como uma tupla, pois caso contrário, os elementos seguintes são interpretados como *None*.
import threading
import time
def count_down(n):
for i in range(n,-1,-1):
print("Executando thread1....")
print(i)
time.sleep(1)
def count_up(n):
for i in range(n+1):
print("Executando thread2...")
print(i)
time.sleep(1)
thread1 = threading.Thread(target=count_down,args=(10,))
thread2 = threading.Thread(target=count_up,args=(5,))
thread1.start()
thread2.start()
Na saída:
- A função *count_up* é executada no *thread2* e conta até 5, começando em 0.
- A função *count_down* é executada no *thread1* e faz a contagem regressiva de 10 até 0.
# Saída
Executando thread1....
10
Executando thread2...
0
Executando thread1....
9
Executando thread2...
1
Executando thread1....
8
Executando thread2...
2
Executando thread1....
7
Executando thread2...
3
Executando thread1....
6
Executando thread2...
4
Executando thread1....
5
Executando thread2...
5
Executando thread1....
4
Executando thread1....
3
Executando thread1....
2
Executando thread1....
1
Executando thread1....
0
Podemos observar que *thread1* e *thread2* são executados de forma alternada, já que ambos incluem uma operação de espera (*sleep*). Depois que a função *count_up* termina a contagem até 5, o *thread2* não está mais ativo. Desta forma, vemos na saída apenas o resultado do *thread1*.
Resumindo
Neste guia, você aprendeu a usar o módulo de *threading* integrado no Python para implementar o *multithreading*. Aqui está um resumo dos principais pontos:
- O construtor *Thread* pode ser usado para criar um objeto de *thread*. Ao usar *threading.Thread(target=
,args=( ))*, um *thread* é criado para executar a função com os argumentos especificados. - O programa Python é executado em um *thread* principal, portanto, os objetos de *thread* que você cria são *threads* adicionais. Você pode usar a função *active_count()* que retorna a quantidade de *threads* ativas no momento.
- Você pode iniciar um *thread* usando o método *start()* e esperar que ele termine a execução utilizando o método *join()*.
Você pode criar mais exemplos, modificando o tempo de espera, experimentando diferentes operações de E/S e muito mais. Não se esqueça de usar multithreading nos seus próximos projetos Python. Boa codificação!🎉