O que é NumPy no Python free-threaded

O Python free-threaded é uma compilação do CPython capaz de executar código sem o Global Interpreter Lock, o GIL. O suporte começou no Python 3.13 como parte do trabalho definido pela PEP 703.

O NumPy é a principal biblioteca de arrays e cálculo numérico do ecossistema Python. Desde a versão 2.1, o projeto declara suporte experimental ao CPython 3.13 com o GIL desativado.

O tema ganhou atenção porque threads livres podem usar vários núcleos da CPU dentro do mesmo processo. Isso abre novas opções para pipelines numéricos, mas não transforma qualquer script em código paralelo e exige cuidado maior com dados compartilhados.

Como funciona

No CPython tradicional, o GIL impede que duas threads executem bytecode Python ao mesmo tempo. Muitas operações internas do NumPy já liberam o GIL, por isso certos cálculos numéricos conseguem rodar em paralelo mesmo no interpretador comum.

Na compilação free-threaded, o GIL pode permanecer desativado também durante trechos de código Python. Estruturas internas usam mecanismos próprios de sincronização, e extensões nativas precisam declarar que são compatíveis com esse modo.

Se uma extensão não estiver preparada, o interpretador pode reativar o GIL ao importá-la e emitir um aviso. Por isso, confirmar a configuração em execução é tão importante quanto instalar o binário correto.

Principais recursos

O ganho central é a possibilidade de usar paralelismo com threads sem criar vários processos. Threads do mesmo processo compartilham o espaço de memória, o que pode reduzir a necessidade de copiar ou serializar grandes arrays.

  • Execução paralela: threads podem ocupar núcleos diferentes quando o GIL está desativado.
  • Arrays independentes: cada worker pode processar seu próprio conjunto de dados.
  • Compatibilidade gradual: pacotes podem declarar suporte ao modo free-threaded.
  • Controle em runtime: o GIL pode ser consultado e, em builds compatíveis, ativado por configuração.
  • Integração com threading: APIs conhecidas, como ThreadPoolExecutor, continuam disponíveis.

Para workloads numéricos, a combinação é interessante quando há várias tarefas independentes. Exemplos incluem processar lotes, executar simulações separadas ou calcular resultados para diferentes arquivos.

O NumPy não adiciona bloqueio automático a cada ndarray. A biblioteca recomenda que cada thread seja dona de seus arrays ou que dados compartilhados sejam tratados como somente leitura.

Como começar: instalação e acesso passo a passo

Passo 1: instale uma compilação free-threaded do Python. Os instaladores oficiais para Windows e macOS oferecem essa opção desde o Python 3.13, enquanto compilações a partir do código-fonte usam a opção --disable-gil.

Passo 2: crie um ambiente isolado e instale uma versão atual do NumPy. Passo 3: verifique tanto a configuração do binário quanto o estado real do GIL no processo.

Python -m pip install numpy
Python -VV
Python -c "import sys; print(sys._is_gil_enabled())"
Python -c "import sysconfig; print(sysconfig.get_config_var('Py_GIL_DISABLED'))"

O primeiro teste identifica a compilação na descrição da versão. O segundo retorna o estado atual do GIL, e a variável Py_GIL_DISABLED indica se o binário tem suporte ao modo free-threaded.

Exemplo prático

O exemplo abaixo cria quatro tarefas independentes. Cada thread inicializa seu próprio gerador aleatório, cria seu próprio array e calcula uma soma sem escrever em dados usados por outra thread.

import numpy as np
from concurrent.futures import ThreadPoolExecutor

def calcular(seed):
    rng = np.random.default_rng(seed)
    dados = rng.random(2_000_000)
    return np.sqrt(dados).sum()

with ThreadPoolExecutor(max_workers=4) as pool:
    resultados = list(pool.map(calcular, range(4)))

print(resultados)

O desenho evita a parte mais perigosa da concorrência: duas threads modificando o mesmo array. Para medir o benefício, compare a execução com um worker e com vários workers no mesmo computador.

O resultado depende do processador, da operação escolhida e das threads internas usadas por bibliotecas numéricas. Aumentar max_workers sem medir pode piorar o tempo por contenção e excesso de concorrência.

Comparação com alternativas

O CPython tradicional continua adequado para a maioria dos projetos. Como diversas operações do NumPy liberam o GIL, tarefas concentradas em código nativo já podem obter paralelismo com threads em situações específicas.

O módulo multiprocessing usa processos separados e oferece isolamento forte. Ele é útil quando um pacote não suporta free-threading, mas pode exigir serialização, cópias de memória e comunicação adicional entre workers.

Vetorização e kernels numéricos otimizados continuam sendo a primeira escolha para uma operação sobre um único array. O modo free-threaded é mais interessante quando o fluxo mistura várias tarefas independentes ou combina lógica Python com chamadas numéricas.

  • CPython com GIL: maior compatibilidade com extensões existentes.
  • Multiprocessing: isolamento por processo e custo maior de comunicação.
  • Python free-threaded: memória compartilhada e paralelismo com responsabilidade explícita.

Pontos positivos e limitações

O ponto positivo mais evidente é usar vários núcleos sem dividir a aplicação em processos. Para arrays grandes, evitar cópias pode simplificar a arquitetura e reduzir custos de comunicação.

O suporte do NumPy ainda é descrito como experimental. Arrays compartilhados que são lidos e modificados ao mesmo tempo podem produzir resultados inconsistentes, e redimensionar um array enquanto outra thread o lê pode até causar falha no interpretador.

Arrays com dtype=np.object_ exigem atenção especial, pois objetos Python deixam de contar com a proteção indireta do GIL. O modo free-threaded também pode consumir mais memória e ter sobrecarga em execução com apenas uma thread.

Casos de uso reais

Simulações independentes são um bom caso. Cada thread pode receber uma semente e seus parâmetros, criar arrays locais e devolver um resultado pequeno para agregação.

Pipelines de dados podem dividir arquivos ou lotes entre workers. O padrão funciona melhor quando cada tarefa tem dados próprios e passa a maior parte do tempo em cálculos que conseguem ocupar a CPU.

Aplicações científicas que alternam lógica Python e operações NumPy também podem se beneficiar. Antes de migrar, é necessário conferir se todas as extensões nativas usadas no ambiente são compatíveis.

  • Monte Carlo: execuções independentes com sementes diferentes.
  • Processamento em lote: um arquivo ou bloco de dados por thread.
  • Pré-processamento: transformações isoladas antes de treinar um modelo.
  • Testes de parâmetros: uma combinação de configuração por worker.

Dicas e boas práticas

Trate a concorrência como uma decisão de arquitetura. Defina quem é dono de cada array, onde os resultados são reunidos e quais objetos podem ser compartilhados apenas para leitura.

💡
Dica

Comece com arrays independentes por thread. Esse padrão reduz bloqueios e elimina grande parte das corridas de dados.

⚠️
Atenção

Não redimensione nem modifique um array enquanto outra thread o utiliza. Use bloqueios próprios quando a escrita compartilhada for inevitável.

🚀
Pro tip

Registre o resultado de sys._is_gil_enabled() nos testes de performance. Uma extensão incompatível pode reativar o GIL durante a importação.

🔴
Cuidado

Evite compartilhar arrays com dtype=np.object_ entre threads sem sincronização. O modo free-threaded permite corridas sobre os objetos armazenados.

Faça benchmarks com o workload real e observe CPU, memória e tempo total. Compare uma thread, várias threads e processos antes de escolher a solução para produção.

Vale a pena?

Vale testar quando o projeto usa NumPy, possui tarefas independentes e sofre com o custo de processos ou com trechos Python que impedem a escala. O potencial é maior em máquinas com vários núcleos e workloads preparados para concorrência.

Não vale migrar apenas porque o GIL pode ser desativado. Compatibilidade de extensões, segurança dos dados compartilhados e desempenho com uma thread precisam entrar na avaliação.

O próximo passo é instalar o build em um ambiente separado, confirmar o estado do GIL e executar um benchmark pequeno com arrays independentes. Se o resultado for consistente, amplie a cobertura de testes antes de usar o modo em produção.