gh-116167: Permitir desativar o GIL
(github.com/python)- O PR #116338 do CPython mesclou em
python:mainuma mudança que permite desativar o GIL em builds free-threaded comPYTHON_GIL=0ou-X gil=0 - Para manter a possibilidade de reativar o GIL em tempo de execução, as estruturas de dados relacionadas ao GIL são inicializadas normalmente, e a desativação é tratada definindo uma flag na inicialização para que
take_gil()edrop_gil()retornem antecipadamente - Nas verificações iniciais, com
PYTHON_GIL=0, alguns testes e pequenos programas que não usam threads funcionaram normalmente, e programas de threads muito básicos às vezes funcionaram, mas a suíte completa de testes crashou rapidamente emtest_asyncio - Durante a revisão, foram adicionados testes de
PYTHON_GIL, documentação, a opção-X gile o reflexo emsys.flags; o tratamento da configuração também foi corrigido para quePYTHON_GIL=1force a ativação do GIL - O trabalho posterior foi separado em questões sobre reativar o GIL ao carregar extensões incompatíveis e desativar o GIL por padrão; esta mudança adiciona uma superfície de controle do GIL nos builds free-threaded do Python 3.13
Mudança mesclada
- O PR #116338 do CPython trata da mudança
gh-116167: Allow disabling the GIL with PYTHON_GIL=0 or -X gil=0 colesburya mesclou empython:mainem 11 de março de 2024- O tamanho da mudança é indicado como 12 arquivos, 163 linhas adicionadas e 1 linha removida
- O recurso-alvo não é para builds comuns, mas uma opção de execução para desativar o GIL em builds free-threaded
Como o GIL é desativado
- Em builds free-threaded, é possível desativar o GIL com as seguintes configurações
PYTHON_GIL=0-X gil=0
- Para permitir que o GIL seja reativado em tempo de execução, todas as estruturas de dados relacionadas ao GIL são inicializadas normalmente
- A desativação real é feita definindo uma flag na inicialização
- Por causa dessa flag,
take_gil()edrop_gil()retornam antecipadamente
- Por causa dessa flag,
- Durante a revisão, também foi adicionado um commit que define corretamente
enable_gilquandoPYTHON_GIL=1
Testes e restrições atuais
- Alguns testes e pequenos programas foram verificados com a configuração
PYTHON_GIL=0- Testes e pequenos programas que não usam threads foram confirmados como funcionando normalmente
- Programas de threads muito básicos às vezes funcionam
- A suíte completa de testes crashou rapidamente, com o local registrado como
test_asyncio - Com o comando
!buildbot nogil, testes de builders relacionados a NoGIL foram agendados várias vezesx86-64 MacOS Intel ASAN NoGIL PRx86-64 MacOS Intel NoGIL PRARM64 MacOS M1 Refleaks NoGIL PRARM64 MacOS M1 NoGIL PRAMD64 Ubuntu NoGIL Refleaks PRAMD64 Ubuntu NoGIL PRAMD64 Windows Server 2022 NoGIL PR
Escopo adicionado durante a revisão
corona10sugeriu que valeria a pena adicionar um teste de variável de ambiente emLib/test/test_cmd_line.py- Depois disso, os seguintes commits foram adicionados
Add test for PYTHON_GIL in test_cmd_lineSet enable_gil properly when PYTHON_GIL=1Don't add 'enable_gil' to test_embed in normal builds
colesburyconsiderou melhor documentar no momento em que a variável de ambiente fosse adicionada- Usou como base o fato de que a flag de configuração
--disable-giljá está documentada - Organizou que a documentação deveria incluir que ela só está disponível em builds free-threaded, que
0força a desativação do GIL, que1força a ativação do GIL e que é uma novidade do Python 3.13
- Usou como base o fato de que a flag de configuração
- Depois disso, foi adicionado o commit
Document PYTHON_GIL environment variable
Adição da opção -X gil e merge final
- Após uma discussão no Discord, decidiu-se adicionar também uma opção
-Xpara uso junto com a variável de ambiente - O título do PR foi alterado de uma forma que tratava apenas de
PYTHON_GIL=0para incluir tambémPYTHON_GIL=0 or -X gil=0 - Os commits adicionais incluem os seguintes itens
Add -X gil option, add to sys.flags, modify test to cover env var… and optionFix link to -X gilFix PYTHON_GIL versionchanged lineClarify test_flags in normal builds
ericsnowcurrently,erlend-aasland,corona10ecolesburyaprovaram a mudança- O commit de merge é
2731913e, após o merge,vstinnerreagiu à mudança dizendo que era “interessante e muito assustadora”
Trabalhos posteriores
- Duas tarefas foram separadas em issues posteriores
- O PR atual não é uma mudança no valor padrão do GIL, mas uma mudança que permite ao usuário controlar o estado do GIL em builds free-threaded por meio de uma variável de ambiente ou da opção
-X
1 comentários
Opiniões no Hacker News
Deixo links adicionais para quem tem curiosidade sobre o trabalho no no-GIL: [0], [1]
[0] Multithreaded Python without the GIL
https://docs.google.com/document/d/18CXhDb1ygxg-YXNBJNzfzZsD...
[1] Repositório no Github
https://github.com/colesbury/nogil
[0] https://peps.python.org/pep-0703/
[1] https://github.com/colesbury/nogil-3.12
Estou ansioso para ver quanto mais rápido o Python padrão pode ficar. A proposta de valor do Python também está sendo desafiada à medida que surgem ferramentas demais tentando mitigar esse problema
Como ferramentas de melhoria de velocidade, me vêm à mente Mojo, pytorch, triton, numba e taichi. Há tantas tentativas de resolver esse problema que, da última vez que tentei usar uma delas, fiquei sobrecarregado com o excesso de opções. Acabei escolhendo taichi, e ele foi bem interessante e fácil de usar, mas seu escopo de aplicação era um tanto limitado
Taichi é realmente subestimado. Funciona em todas as plataformas, incluindo Metal, tem muitos exemplos e é fácil escrever código. Acima de tudo, integra-se ao ecossistema e não substitui o ecossistema existente
https://github.com/taichi-dev
Um ótimo vídeo de demonstração que mostra o que é possível fazer com Taichi: https://www.youtube.com/watch?v=oXRJoQGCYFg
https://www.youtube.com/watch?v=WNh4Q7-OSJs
https://www.taichi-lang.org/
Fico curioso por que o método de contagem de referências enviesada descrito em https://peps.python.org/pep-0703/ mantém afinidade com apenas uma thread e exige incrementos/decrementos atômicos quando acessado por outras threads
Em outras implementações, por exemplo em vários crates Rust que implementam contagem de referências enviesada, vi uma abordagem em que, ao mover para uma nova thread, incrementa-se atomicamente apenas nesse momento, e então essa thread faz incrementos/decrementos não atômicos até chegar novamente a 0, realizando um decremento atômico no final. Fico me perguntando se isso é porque, sendo algo enxertado em um sistema existente, há um único PyObject e não dá para substituí-lo por algo que aponte para um novo objeto local da thread
Em Rust, o "move" para transferência de propriedade faz parte da linguagem, mas em C ou Python não há um conceito correspondente, então é difícil determinar quando a propriedade deve ser transferida e qual thread deve se tornar a nova proprietária. Dá para usar heurísticas. Por exemplo, ao colocar um objeto em uma queue.SimpleQueue, a propriedade poderia ser abandonada ou transferida, mas mesmo nesse caso é difícil saber de antemão qual thread fará o "get" do objeto na fila
O ganho de desempenho também parece pequeno. Muitos objetos são acessados por apenas uma thread, e alguns objetos são acessados por várias threads, mas é raro um objeto ser acessado exclusivamente por uma thread e depois exclusivamente por outra
Primeiro li a notícia sobre tranched bread, e agora isso também? Que época incrível
Fiquei um pouco decepcionado quando o projeto Unladen Swallow [1] acabou perdendo força. É bom ver o Python voltando ao caminho de otimizações centrais
[1] https://en.wikipedia.org/wiki/CPython#Unladen_Swallow
Gostaria que explicassem como se eu tivesse cinco anos
Conceitualmente, entendo o que é o GIL. Mas qual é o impacto dessa mudança? Agora os pacotes vão quebrar enquanto esperamos uma melhoria geral de desempenho?
Mesmo quando não se trata de trabalho pesado de CPU, essa mudança pode ser útil. Hoje, muito código é escrito com os recursos nativos de linguagem asyncio do Python. Isso funciona em uma única thread, cedendo a execução com async/await, como no NodeJS, e consegue uma vazão bastante boa, na casa de milhares de requisições por segundo, mesmo usando apenas uma thread
Mas o grande problema é que, no momento em que qualquer tarefa de CPU é executada, ela bloqueia todas as outras corrotinas, criando todo tipo de problema obscuro e destruindo o número de requisições por segundo. Por exemplo, você pode ver timeouts aleatórios de I/O em uma corrotina, quando a causa real é uma corrotina completamente diferente que ocupou a CPU por um breve período. Também é muito difícil observar por que isso acontece. O asyncio oferece a função
asyncio.to_thread()[1], que ajuda a mover trabalho bloqueante para fora da thread principal, mas, por causa do GIL, ela não separa de verdade tarefas centradas em CPU de modo que não interfiram nas outras corrotinas[1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
Para quem tiver curiosidade, GIL é a sigla de Global Interpreter Lock
Há algum material que organize bem o panorama geral aqui?
Finalmente estou ansioso pelos benchmarks de várias ferramentas