Estado Compartilhado e Limites de Isolamento
Python oferece threads, processos, concurrent.futures, subinterpreters e agora um build free-threaded - o que pode parecer uma caixa de ferramentas incomumente grande para um único problema.
Na verdade, é um pequeno número de ferramentas posicionadas em um espectro: quanta memória duas partes de código concorrente têm permissão para compartilhar, e qual mecanismo impede que essa memória compartilhada seja corrompida.
Cada ferramenta nesta seção é uma resposta específica para essa mesma pergunta, não um recurso não relacionado.
Fundamentos de Concorrência introduz a distinção concorrência versus paralelismo, e Escolhendo um Modelo de Concorrência fornece uma lista de verificação prática de decisão; threading, multiprocessing, O GIL e Python Free-Threaded e Subinterpreters cobrem cada ponto específico no espectro em profundidade.
Esta página é a camada abaixo de todas elas: o único eixo em que todas elas têm posições diferentes, e por que esse eixo é o que realmente decide qual ferramenta se adapta a uma carga de trabalho específica.
Resumo
- Cada mecanismo de concorrência do Python faz um trade-off ao longo de um eixo - quanta memória é compartilhada entre unidades de trabalho concorrentes e o que garante a segurança dessa memória compartilhada.
- Por que Importa: Escolher o ponto errado nesse espectro deixa o paralelismo real na mesa (usando threads para trabalho intensivo de CPU) ou incorre em um custo de isolamento desnecessário (usando processos para trabalho intensivo de I/O que threads lidam bem).
- Conceitos Chave: memória compartilhada, limite de isolamento, o GIL, processo, estado do interpretador, free-threading.
- Quando Usar: Escolher entre threads, processos, subinterpreters ou async para uma determinada carga de trabalho, e entender por que a mesma tarefa ("executar isso concorrentemente") requer ferramentas diferentes dependendo se é intensiva em CPU ou em I/O.
- Limitações / Trade-offs: Mais isolamento garante mais paralelismo real, mas custa mais overhead - um processo é mais pesado que uma thread, e a comunicação entre limites (serialização, sockets, IPC) nunca é tão barata quanto passar um objeto simples por referência.
- Tópicos Relacionados: o Global Interpreter Lock,
multiprocessing, executoresconcurrent.futures, subinterpreters, builds free-threaded do Python.
Fundamentos
Duas ou mais partes de código rodando "ao mesmo tempo" sempre têm que responder a uma pergunta fundamental: o que acontece se ambas tentarem ler ou modificar a mesma parte da memória de uma vez?
O modelo de concorrência de uma linguagem é, em sua essência, um conjunto de respostas para essa pergunta, e Python oferece várias porque cargas de trabalho diferentes querem respostas diferentes.
Em um extremo está o compartilhamento total com um único lock: uma thread roda dentro do mesmo processo que todas as outras threads, vê os mesmos objetos na memória, e no build CPython padrão, apenas uma thread executa bytecode Python a cada instante porque o Global Interpreter Lock (GIL) impõe essa regra.
No outro extremo está o nenhum compartilhamento: um processo (via multiprocessing) tem seu próprio espaço de memória privado, seu próprio interpretador e seu próprio GIL, então dois processos podem executar bytecode Python genuinamente simultaneamente em núcleos separados - o preço é que eles não veem automaticamente os objetos uns dos outros, e qualquer coisa passada entre eles tem que ser serializada (pickled) através do limite.
Uma analogia útil: threads são colegas de trabalho em um escritório compartilhado, capazes de alcançar o mesmo arquivo instantaneamente, mas apenas um de cada vez por regra da casa; processos são escritórios separados em prédios diferentes, cada um com seu próprio arquivo, capazes de trabalhar simultaneamente, mas precisando de um mensageiro (serialização) para trocar qualquer papelada.
# threads: mesma memória, mesmos objetos, um GIL - seguro por serialização, não isolamento
counter = {"n": 0}
# cada thread vê este dicionário exato; um lock é necessário para incrementá-lo com segurança
# processos: memória separada, objetos separados - seguro por isolamento, não por lock
# um processo filho recebe sua própria cópia; mudanças não aparecem no pai automaticamenteMecanismos e Interações
O GIL é o mecanismo que torna o modelo padrão do CPython, de memória totalmente compartilhada, seguro: ele garante que apenas uma thread execute bytecode Python por vez, o que significa que a maioria das operações únicas em objetos Python não falha ou corrompe sem um lock explícito, ao custo de threads nunca executarem código Python verdadeiramente em paralelo umas com as outras.
Esse único fato explica a divisão que esta seção continua a retornar: threads ajudam o trabalho intensivo de I/O, porque operações de I/O liberam o GIL enquanto esperam pela rede ou disco, permitindo que outra thread rode durante essa espera - mas threads não ajudam o trabalho intensivo de CPU, porque um loop Python apertado nunca libera o GIL, então N threads fazendo computação pura não rodam mais rápido que uma.
multiprocessing contorna o GIL inteiramente dando a cada worker seu próprio interpretador e seu próprio GIL, que é por que é a resposta padrão para paralelismo intensivo de CPU no CPython comum - o isolamento que torna a serialização necessária é o mesmo isolamento que torna a execução paralela real possível.
concurrent.futures não introduz um novo ponto neste espectro; é uma interface Executor uniforme (submit, map) sobre duas existentes - ThreadPoolExecutor para o lado da memória compartilhada, ProcessPoolExecutor para o lado isolado - então alternar entre elas é uma mudança de uma linha depois que o código é escrito contra a API Executor em vez de contra threads ou processos diretamente.
Memória compartilhada, um GIL Memória isolada, um GIL cada
------------------------- ------------------------------
threading.Thread multiprocessing.Process
ThreadPoolExecutor ProcessPoolExecutor
barato para iniciar, barato para compartilhar caro para iniciar, deve serializar para compartilhar
trabalho intensivo de I/O trabalho intensivo de CPU
Filas (queue.Queue para threads, multiprocessing.Queue para processos) existem porque "isolado" e "compartilhado" ainda precisam de uma maneira segura de passar dados através do limite - uma fila thread-safe coordena o acesso dentro da memória compartilhada, enquanto uma fila process-safe realmente serializa cada item através do limite do processo, fazendo mais trabalho por item precisamente porque não há memória para compartilhar em primeiro lugar.
Considerações Avançadas e Aplicações
Dois desenvolvimentos mais recentes no CPython relaxam o padrão "um GIL, totalmente compartilhado" sem se mover totalmente para processos separados, e ambos são melhor entendidos como novos pontos no mesmo espectro em vez de conceitos totalmente novos.
Subinterpreters (utilizáveis do Python via APIs relacionadas à PEP 734, 3.12+/3.13+) dão a cada interpretador seu próprio GIL e, por padrão, seu próprio estado isolado, enquanto ainda vivem dentro de um único processo OS - um ponto intermediário entre "um GIL compartilhado" e "processos totalmente separados", trocando parte do custo de inicialização e IPC do multiprocessing por um isolamento mais leve que um processo completo, ao custo de a maioria dos objetos ainda não ser diretamente compartilhável através do limite.
O CPython Free-Threaded (3.13+, experimental) adota a abordagem oposta: em vez de isolar a memória para evitar a necessidade de um único lock global, ele substitui o GIL único por locks mais granulares, por objeto, para que threads realmente possam executar bytecode Python em paralelo enquanto continuam a compartilhar memória diretamente - o mesmo modelo de memória compartilhada da thread comum, mas com a segurança anteriormente fornecida por um grande lock agora distribuída por muitos menores.
Essa única mudança é significativa o suficiente para justificar cautela real: extensões C escritas assumindo que um GIL as protegia implicitamente podem não ser seguras sob um build free-threaded, a menos que sejam atualizadas para declarar e impor sua própria thread-safety, que é por que adotar um build sem GIL em produção significa auditar a árvore de dependências, não apenas mudar uma flag de build.
| Modelo | Memória | Mecanismo de segurança | Melhor Ajuste |
|---|---|---|---|
| Threads (GIL padrão) | Totalmente compartilhada | Um GIL serializa a execução do bytecode | Trabalho intensivo de I/O; compartilhar objetos de forma barata é mais importante que paralelismo de CPU |
Processos (multiprocessing) | Isolado por worker | Limite de processo em nível de OS; nenhum estado compartilhado por padrão | Trabalho intensivo de CPU que necessita de execução paralela real entre núcleos |
| Subinterpreters | Isolado por interpretador, um processo | GIL por interpretador | Isolar código não confiável ou estilo plugin sem o overhead de um processo completo |
| Build Free-Threaded | Totalmente compartilhada | Locks por objeto em vez de um lock global | Cargas de trabalho com threads intensivas em CPU, uma vez que a árvore de dependências seja verificada como thread-safe |
O resultado prático é que "qual ferramenta de concorrência devo usar" é realmente "quanto isolamento essa carga de trabalho precisa, e quanto posso pagar por isso" - Escolhendo um Modelo de Concorrência transforma isso em uma lista de verificação concreta, mas a lista de verificação só faz sentido quando esse trade-off memória compartilhada versus isolamento é a lente pela qual você a está vendo.
Equívocos Comuns
- "Threads em Python não rodam realmente concorrentemente." Elas rodam concorrentemente - o GIL apenas impede que duas threads executem bytecode Python no exato mesmo instante, mas esperas de I/O, extensões C que liberam o GIL e o escalonamento em nível de OS ainda permitem que threads façam progresso real e sobreposto.
- "Multiprocessing é apenas threading, mas mais pesado, sem outro benefício." O overhead compra algo que o threading estruturalmente não pode sob o GIL padrão: execução simultânea genuína de bytecode Python entre núcleos de CPU.
- "Subinterpreters são a mesma coisa que multiprocessing, apenas dentro de um processo." Eles são isolados como processos em termos de estado padrão, mas compartilham o processo OS e seu espaço de memória de uma maneira que os processos separados do multiprocessing não fazem, o que muda tanto o custo de inicialização quanto o que pode eventualmente ser compartilhado com segurança entre eles.
- "Python Free-Threaded remove a necessidade de pensar em thread-safety." Ele remove o GIL único como mecanismo de segurança, mas o substitui pela necessidade de código e extensões genuinamente thread-safe - o fardo da segurança se move, não desaparece.
- "
concurrent.futuresé um terceiro modelo de concorrência, separado de threads e processos." É uma interface, não um modelo -ThreadPoolExecutoreProcessPoolExecutorsão o mesmo trade-off threads vs. processos descrito aqui, embrulhado em uma API consistente.
FAQs
Qual é a única pergunta fundamental que toda ferramenta de concorrência do Python está respondendo?
Quanta memória é compartilhada entre partes de código rodando concorrentemente, e qual mecanismo mantém essa memória compartilhada segura - compartilhamento total sob um lock, ou isolamento total sem necessidade de lock.
O GIL é um recurso de performance ou um recurso de segurança?
Segurança. Ele garante que apenas uma thread execute bytecode Python por vez, o que mantém as estruturas internas de objetos do CPython consistentes sem que cada operação precise de seu próprio lock explícito - o trade-off de performance (nenhum paralelismo de CPU entre threads) é o custo dessa garantia de segurança, não seu propósito.
Por que threads ajudam com chamadas de rede, mas não com loops intensivos de CPU?
Operações de I/O liberam o GIL enquanto esperam, permitindo que outra thread rode durante essa espera - um loop de computação Python apertado nunca atinge um ponto onde libera o GIL, então outras threads simplesmente esperam sua vez em vez de rodar em paralelo.
Por que multiprocessing requer serializar (pickling) dados entre processos?
Porque cada processo tem seu próprio espaço de memória separado e seu próprio interpretador - não há memória compartilhada que um processo filho possa ler diretamente do pai, então qualquer dado que cruza esse limite tem que ser convertido para uma forma transportável e reconstruído do outro lado.
O `concurrent.futures` é um modelo de concorrência diferente de threading e multiprocessing?
Não - é uma interface Executor uniforme sobre os mesmos dois modelos subjacentes. ThreadPoolExecutor usa threads e memória compartilhada; ProcessPoolExecutor usa processos e memória isolada; alternar entre eles é, na maioria das vezes, uma mudança de uma linha.
Que problema os subinterpreters realmente resolvem?
Eles oferecem isolamento semelhante a processos separados - cada um com seu próprio GIL e, por padrão, seu próprio estado - enquanto permanecem dentro de um único processo OS, o que é mais leve do que iniciar processos completos para cargas de trabalho que precisam principalmente de isolamento em vez de acesso a memória compartilhada.
Python Free-Threaded significa que threads não precisam mais de locks?
Não - ele remove o único lock global e o substitui por locks mais granulares, por objeto, então o acesso genuinamente concorrente a dados compartilhados ainda é possível e o código (e extensões C) ainda precisa ser escrito para ser thread-safe sob esse modelo mais granular.
Por que não posso simplesmente usar multiprocessing sempre para ser seguro, já que ele evita o GIL completamente?
Processos são mais pesados para iniciar e requerem serialização de dados através do limite, o que adiciona overhead real - para trabalho intensivo de I/O, onde o GIL não é o gargalo em primeiro lugar, esse overhead não compra nada e threads (ou async) são o ajuste mais barato.
As filas são diferentes entre threading e multiprocessing?
Funcionalmente semelhantes na superfície, mas mecanicamente diferentes: queue.Queue coordena o acesso seguro a objetos compartilhados na memória entre threads, enquanto multiprocessing.Queue realmente serializa cada item através do limite do processo, já que não há memória para compartilhar diretamente.
Adotar um build free-threaded é uma mudança "drop-in" para um codebase existente?
Não com segurança, sem auditoria prévia - extensões C escritas assumindo que o GIL as protegia implicitamente podem não ser thread-safe sob um build onde esse lock único não existe mais, então verificar a árvore de dependências é importante antes de confiar nele em produção.
Como devo decidir entre threads e processos para uma determinada tarefa?
Pergunte se a carga de trabalho é intensiva em I/O ou em CPU: trabalho intensivo de I/O se beneficia do compartilhamento barato das threads, já que o GIL não é a restrição, enquanto trabalho intensivo de CPU precisa do paralelismo real que apenas processos isolados (ou um build free-threaded verificado) podem fornecer.
Subinterpreters e Python free-threaded resolvem o mesmo problema?
Eles abordam a mesma limitação subjacente - um único GIL serializando tudo - de direções opostas: subinterpreters adicionam isolamento para contornar o compartilhamento do lock, enquanto free-threading remove o lock único, mas mantém a memória totalmente compartilhada.
Relacionados
- Fundamentos de Concorrência - concorrência vs. paralelismo e escolha de uma ferramenta inicial
- O GIL e Python Free-Threaded - o mecanismo do GIL e o build sem GIL em profundidade
- multiprocessing - processos isolados para paralelismo intensivo de CPU
- threading - concorrência de memória compartilhada para trabalho intensivo de I/O
- Subinterpreters - isolamento por interpretador dentro de um processo
- Escolhendo um Modelo de Concorrência - transformando esse trade-off em uma lista de verificação de decisão
Versões do Stack: Esta página foi escrita para Python 3.14.0 (estável 3.14, manutenção 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, e uv 0.6+.