Dominando Multithreading em Python: GIL Explicado com Exemplos Práticos
Python oferece tanto multiprocessing quanto multithreading para acelerar cargas de trabalho. Este guia percorre a criação de aplicações robustas baseadas em threads, as nuances do Global Interpreter Lock (GIL) e como evitar armadilhas comuns como deadlocks e race conditions.
O que é uma Thread?
Uma thread é uma unidade leve de execução dentro de um único processo. Threads compartilham o espaço de memória do processo, permitindo troca eficiente de dados, enquanto o sistema operacional as agenda para rodar concorrentemente em um ou mais núcleos de CPU.
O que é um Processo?
Um processo é uma instância de programa independente. Quando você lança uma aplicação — digamos um navegador ou editor de texto — o OS cria um processo separado com seu próprio código, dados e mapa de memória.
Fundamentos de Multithreading em Python
O módulo threading do Python fornece uma API de alto nível para gerenciar threads, enquanto o módulo legado _thread é depreciado e retido apenas para compatibilidade retroativa. Threads são ideais para tarefas I/O-bound, enquanto trabalho CPU-bound deve usar o módulo multiprocessing para contornar o GIL.
- Thread: memória compartilhada, leve.
- Processo: memória isolada, mais pesado.
- GIL: um mutex que protege execução de bytecode Python.
Quando Usar Multithreading
Multithreading brilha quando você precisa realizar várias operações I/O-pesadas — requisições de rede, I/O de arquivo ou chamadas de banco de dados — simultaneamente. Threads bem projetadas podem melhorar responsividade e throughput sem o overhead de spawnar novos processos.
Principais Módulos Python para Threading
- _thread — Low-level, depreciado.
- threading — Moderno, rico em recursos e a escolha recomendada.
Usando o Módulo threading
O módulo threading oferece uma classe Thread, primitivas de sincronização (Lock, RLock, Semaphore, Condition, Event, Barrier) e funções utilitárias como activeCount() e enumerate(). Abaixo está um exemplo mínimo demonstrando criação de thread e sincronização.
import time
import _thread
def thread_test(name, wait):
for i in range(4):
time.sleep(wait)
print(f"Running {name}")
print(f"{name} has finished execution")
if __name__ == "__main__":
_thread.start_new_thread(thread_test, ("First Thread", 1))
_thread.start_new_thread(thread_test, ("Second Thread", 2))
_thread.start_new_thread(thread_test, ("Third Thread", 3))
Executar este código produz saída intercalada demonstrando execução concorrente.

Explicação
- Importe
timee o legado_thread. - Defina
thread_testpara realizar trabalho e logar progresso. - Inicie três threads independentes via
start_new_thread.
Threading Moderno com threading.Thread
Na prática, você fará subclass de threading.Thread e sobrescreverá seu método run(). O exemplo a seguir espelha o comportamento anterior mas usa a API de alto nível.
import time
import threading
class ThreadTester(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name = name
self.delay = delay
def run(self):
for _ in range(4):
time.sleep(self.delay)
print(f"Running {self.name}")
print(f"{self.name} has finished execution")
if __name__ == "__main__":
threads = [
ThreadTester("First Thread", 1),
ThreadTester("Second Thread", 2),
ThreadTester("Third Thread", 3),
]
for t in threads:
t.start()
for t in threads:
t.join()
Saída:

Explicação
- Subclasse
Threadpara encapsular comportamento de thread. - Sobrescreva
__init__para armazenar dados específicos da thread. - Sobrescreva
runpara definir a tarefa. - Instancie, inicie e dê join em cada thread.
Armadilhas Comuns de Threading
Deadlocks
Um deadlock ocorre quando duas ou mais threads esperam indefinidamente por recursos mantidos uma pela outra. Ilustração clássica: o problema dos Filósofos Jantando — cinco filósofos competem por cinco garfos, cada um precisando de dois garfos para comer. Se todo filósofo pegar o garfo da direita primeiro, uma espera circular surge, paralisando todo progresso.
Race Conditions
Quando múltiplas threads modificam dados compartilhados sem coordenação adequada, o estado final pode ser imprevisível. Por exemplo:
i = 0
for _ in range(100):
print(i)
i += 1
Executando este loop em threads paralelas pode produzir sequência embaralhada porque incrementos se sobrepõem.
Sincronizando Threads
O módulo threading fornece várias primitivas de sincronização. A mais fundamental é Lock, que garante acesso exclusivo a um recurso compartilhado.
import threading
lock = threading.Lock()
def worker(name):
for _ in range(5):
lock.acquire()
print(f"{name} acquired lock")
# critical section
lock.release()
if __name__ == "__main__":
t1 = threading.Thread(target=worker, args=("Thread A",))
t2 = threading.Thread(target=worker, args=("Thread B",))
t1.start(); t2.start()
t1.join(); t2.join()
Saída demonstra acesso serializado à seção crítica.

Outras Ferramentas de Sincronização
- RLock — lock re-entrante.
- Semaphore — controla acesso a número finito de recursos.
- Condition — permite threads esperarem por condições específicas.
- Event — flag simples para sinalização inter-thread.
- Barrier — bloqueia um conjunto de threads até que todas atinjam um ponto.
Entendendo o Global Interpreter Lock (GIL)
CPython usa o GIL para serializar execução de bytecode entre threads. Embora isso proteja estruturas internas do interpretador (ex: reference counting) de race conditions, também limita paralelismo para tarefas CPU-bound em sistemas multi-core.
- Quando uma thread inicia, ela adquire o GIL.
- Se outra thread precisa do GIL, ela bloqueia até a primeira liberá-lo.
- Threads I/O-bound liberam o GIL durante chamadas bloqueantes, permitindo outras rodarem.
- Threads CPU-bound seguram o GIL, impedindo outras threads de executarem código Python.
Consequentemente, multithreading em Python rende pouco benefício para cargas de trabalho computacionalmente pesadas; multiprocessing é a abordagem preferida nesses casos.
Por que o GIL Existe
O garbage collector do CPython depende de reference counting. Sem um lock global, atualizações concorrentes de contadores de referência levariam a race conditions e corrupção de memória. Um lock por objeto introduziria overhead significativo, então o GIL foi adotado como solução simples e efetiva.
Conclusões Práticas
- Use
threadingpara concorrência I/O-bound. - Prefira
multiprocessingpara paralelismo CPU-bound. - Sempre sincronize dados compartilhados com locks ou primitivas de nível superior.
- Esteja ciente do impacto do GIL ao escalar entre núcleos.
- Subclasse
Threade sobrescrevarun()para código limpo e sustentável.
python
- Dominando a Função free() em C: Guia Prático e Exemplo
- Dominando o Método strip() do Python: Guia Completo e Exemplos Práticos
- Domine o str.count() do Python: Como Contar Caracteres e Substrings com Exemplos
- Função round() em Python Explicada com Exemplos Práticos
- Dominando a Função map() do Python: Sintaxe, Exemplos e Boas Práticas
- Python timeit(): Medindo Tempo de Execução com Exemplos Práticos
- Counter do Python em collections: Contagem Eficiente, Atualização e Operações Aritméticas
- Python list.count(): Guia Especializado com Exemplos Práticos
- Python List index(): Como Encontrar Posições de Elementos com Exemplos Práticos
- Dominando Multithreading em C#: Aumente Performance e Eficiência