Intenção de aprovar a PEP 703 para tornar o GIL do CPython opcional
(discuss.python.org)- O Python Steering Council pretende aceitar a PEP 703 para tornar o GIL opcional no CPython, e a reação da comunidade tanto à proposta no-GIL quanto à PEP 703 tem sido, em geral, positiva
- O objetivo de longo prazo é convergir para um build sem GIL; querem evitar um cenário em que os builds with-GIL e no-GIL, assim como o ecossistema de módulos de extensão, fiquem permanentemente divididos
- Durante a transição, a compatibilidade retroativa é a principal restrição, e código de terceiros alterado para suportar no-GIL também deve continuar funcionando em builds with-GIL
- O plano é uma transição em 3 fases: build experimental no curto prazo, build suportado no médio prazo e build padrão no longo prazo; o build experimental pode entrar no Python 3.13, mas não seria um problema se fosse adiado para o 3.14
- Antes de mudar o build padrão, será preciso confirmar o suporte da comunidade e a experiência com API, empacotamento e distribuição; se a confusão for maior do que os benefícios, será necessário interromper a PEP 703 e buscar outra solução
Direção do Steering Council para aceitar a PEP 703
- O Python Steering Council tem a intenção de aceitar a PEP 703
- Na pesquisa sobre a proposta no-GIL, a reação foi amplamente positiva, e o Steering Council também vê de forma geralmente favorável tanto a ideia geral quanto a própria PEP 703
- No entanto, os detalhes da aceitação ainda estão sendo trabalhados e devem ser finalizados nas próximas semanas
Princípios da transição sem uma bifurcação permanente
- No longo prazo, provavelmente ao longo de mais de 5 anos, o build no-GIL deverá se tornar o único build
- Não querem uma situação em que os builds with-GIL e no-GIL permaneçam separados para sempre
- Também querem evitar que o ecossistema de módulos de extensão fique dividido permanentemente entre os dois builds
- A compatibilidade retroativa deve ser tratada com muito cuidado
- Não querem criar outra situação de transição do Python 3
- Mesmo que código de terceiros seja alterado para suportar o build no-GIL, essas mudanças devem continuar funcionando também no build with-GIL
- Questões de compatibilidade com versões anteriores do Python devem ser tratadas separadamente
- Isto não é o Python 4
- Os requisitos de compatibilidade de ABI, os detalhes dos dois builds e o impacto sobre a compatibilidade retroativa ainda estão em análise
Condições a verificar antes da mudança do padrão
- Antes de tornar o build no-GIL o padrão, será preciso confirmar se há suporte suficiente da comunidade
- Não dá para simplesmente mudar o padrão e deixar a comunidade descobrir por conta própria o trabalho necessário
- Os desenvolvedores centrais precisam experimentar diretamente o novo modo de build e tudo ao seu redor
- Ao organizar a segurança de threads do código existente, pode ser necessário um novo C API e uma nova Python API
- Os aprendizados desse processo devem ser compartilhados com a comunidade Python
- É preciso verificar se as mudanças desejadas pelos desenvolvedores centrais e as mudanças exigidas da comunidade estão em um nível aceitável
- Até que no-GIL se torne o padrão, deve ser possível mudar de direção se as mudanças causarem confusão demais e poucos benefícios
- Nesse caso, também deve ser possível decidir reverter todo o trabalho
- Por isso, código exclusivo para no-GIL deve ser, em certo grau, identificável
Plano de transição em 3 fases
- No curto prazo, o build no-GIL será adicionado como modo de build experimental
- Talvez entre no Python 3.13
- Não seria um problema se fosse adiado para o Python 3.14
- O status experimental serve para deixar claro que os desenvolvedores centrais apoiam esse modo de build, mas que não se pode esperar suporte imediato da comunidade
- Será necessário tempo para viabilizar o suporte da comunidade em termos de design de API, empacotamento e distribuição
- Não é recomendado que distribuições ofereçam o build experimental no-GIL como interpretador padrão
- No médio prazo, o build no-GIL passará a ser suportado, mas ainda não será o padrão
- Será necessário ter confiança de que o suporte da comunidade é suficiente para uso em produção
- Nessa fase, será definida uma data-alvo ou uma versão do Python para tornar no-GIL o padrão
- O momento dependerá fortemente da compatibilidade retroativa das mudanças de API, do tratamento da stable ABI e do volume de trabalho que a comunidade considerar necessário
- Isso pode levar pelo menos 1 a 2 anos, ou até mais
- Depois de declarado como suportado, alguns distribuidores poderão começar a oferecer no-GIL como padrão, mas isso pode depender de quantos pacotes Python suportarem no-GIL naquele momento
- No longo prazo, a intenção é tornar no-GIL o padrão e remover os vestígios do GIL
- Não haverá quebra de compatibilidade retroativa sem necessidade
- Manter por muito tempo dois modos de build comuns pode aumentar a carga sobre a comunidade, como dobrar recursos de teste e cenários de depuração
- Ao mesmo tempo, não dá para apressar esse processo, e pode levar até 5 anos para chegar a essa fase
Reavaliação contínua e possibilidade de interrupção
- Ao longo de todo o processo, não só o Steering Council, mas também os desenvolvedores centrais devem reavaliar continuamente o andamento e o cronograma proposto
- Não querem que esse trabalho se transforme em outra luta de 10 anos por compatibilidade retroativa
- Se a PEP 703 começar a mostrar sinais de que será problemática, deve ser possível interrompê-la e buscar outra solução
- Será necessário verificar regularmente se o trabalho em andamento realmente vale a pena
1 comentários
Comentários do Hacker News
Durante décadas, muito código de bibliotecas C vinha com avisos nos manuais dizendo que era instável em contextos assíncronos, reentrantes e recursivos.
Mesmo assim, aprendemos a lidar com isso, e versões seguras para reentrância foram sendo distribuídas gradualmente sem causar grande instabilidade nas APIs.
Havia coisas como parsing de strings que tokenizava no próprio buffer, chamadas de DNS que usavam buffers estáticos e código que dependia do comportamento de pilha específico do Vax; na minha visão, o GIL foi ao mesmo tempo uma bênção e uma maldição.
Acho que isso me criou o hábito de consultar a documentação até de APIs que eu conhecia razoavelmente, por receio de ter perdido algum detalhe importante ou de algo ter mudado.
Naquela época, eu fazia C++ multiplataforma e, quando vi Java pela primeira vez, ele já trazia desde o início concorrência, garbage collection e vários recursos mais fáceis de usar que em C++; tive certeza de que ele cresceria muito.
Depois, desenvolvedores mainstream começaram a usar Python; pelo que me lembro, ele era simples por ser originalmente uma linguagem de extensão embutível, e por isso o GIL fazia mais sentido.
Não é como se alguém fosse acionar um interruptor e quebrar de uma vez uma enorme quantidade de código C duvidoso.
Agora já existem CPUs de 128 núcleos, e até CPUs baratas têm 6 núcleos; portanto, a limitação de ficar preso ao desempenho de um único núcleo só tende a pesar mais com o tempo.
No começo, acreditei que fossem problemas que se resolveriam em algumas semanas, no máximo alguns meses; eu simplesmente não sabia de nada.
Em C, a interação com funções que não são thread-safe é muito mais direta, e quem usa C geralmente toma mais cuidado.
Em Python, há módulos C inteiros com estado global; depois de carregar uns 10 deles e somar a complexidade do interpretador, logo ninguém sabe mais o que está acontecendo.
Mesmo hoje, a maioria dos desenvolvedores, até mesmo desenvolvedores core, não verifica vazamentos de memória; não acho provável que rodem tsan, e, se rodarem, provavelmente será com um conjunto pequeno de testes cobrindo só 10% do código.
Olhando para as práticas de desenvolvimento de software em Python, especialmente na área de IA, sou muito pessimista quanto a esse recurso.
É interessante.
Python é composto em grande parte por bibliotecas compartilhadas em C escritas com a compreensão de que podiam depender de um bloqueio global.
Algumas são simples o bastante para funcionar bem sem locks, mas outras ainda precisam de locks e agora serão pressionadas a executar sem o GIL.
Parte delas provavelmente vai implementar seus próprios locks dentro do seu escopo, e talvez o que realmente faltava ao Python fossem chamadas improvisadas a mutex espalhadas por todo o ecossistema.
Eu não esperava ver Python ser estragado pela introdução de data races e deadlocks em nome de desempenho.
Tornar thread-safe uma biblioteca C escrita presumindo um lock global é o tipo de tarefa que até especialistas em concorrência desaconselhariam, e na qual é fácil cometer erros durante a implementação.
Minha hipótese é que a maioria das pessoas que escreveram extensões C para Python não é especialista em concorrência, mas sim ótimos programadores que não fogem de desafios; com essa combinação, data races/travamentos/segfaults parecem quase inevitáveis.
Mas a expectativa de transformá-lo em opt-in daqui a 5 anos é otimista demais.
Todos os desenvolvedores de bibliotecas terão que corrigir suas próprias bibliotecas, inclusive bibliotecas Python, o que é um trabalho difícil; mesmo quando bem-feito, ninguém percebe, então é difícil ser recompensado por isso.
Muitas bibliotecas nunca tiveram nenhum caso de uso com multiprocessamento, e bibliotecas grandes inevitavelmente terão bugs sutis; reclamações impossíveis de reproduzir e a desistência dos desenvolvedores parecem um resultado garantido.
Mesmo Python com GIL dá suporte a threads, portanto essas bibliotecas devem ser pelo menos seguras para reentrância.
Se uma biblioteca for reentrante, mas não thread-safe, talvez baste adicionar um único lock global envolvendo todas as chamadas, o que é bem parecido com o que o GIL fazia.
Fazer bibliotecas existentes funcionarem sem o GIL parece relativamente direto em muitos casos, embora o paralelismo possa ser sacrificado.
O principal problema provavelmente serão bibliotecas que fazem callback do lado C para o runtime do Python.
Pergunta ingênua, mas, como existem os pacotes asyncio e multiprocessing, fico pensando quem precisaria de No-GIL
Nunca tive problemas em Python por causa do GIL; sempre contornei criando um ThreadPool ou ProcessPool, ou usando uma biblioteca assíncrona quando necessário
Fico curioso se existe algum caso de uso de No-GIL que não seja resolvido com multiprocessing
Eu achava que a execução em thread única, sem o overhead das ferramentas primitivas de concorrência, era a melhor para computação de alto desempenho. Como o LMAX Disruptor mostrou
asyncio é essencialmente single-thread, portanto usa um único core; multiprocessing usa múltiplos cores, então é melhor para desempenho, mas cada processo é relativamente pesado e há overhead adicional de memória compartilhada
Multithreading baseado em GIL usa um único core e ainda é difícil de usar corretamente
Multithreading sem GIL usa múltiplos cores, mas é difícil de usar; não sei sobre a implementação, mas a memória compartilhada deveria ser mais rápida que multiprocessing
Se eu estivesse projetando um sistema novo, concordaria em evitar threads em quase todos os casos de uso de Python e usar asyncio/multiprocessing
Programas Python que precisam de multithreading rápido muitas vezes nem deveriam ter sido escritos em Python desde o início, mas como já há pessoas que escreveram código CPU-intensivo em Python, No-GIL é algo prático
Imagine um servidor web que responde simultaneamente a vários clientes usando estado compartilhado; multiprocessing usa pickle para enviar e receber dados, o que tem grande overhead de desempenho
Por exemplo, se você quiser manter uma estrutura de dados de 1 GB em memória e fazer cálculos paralelos, é difícil obter bom desempenho com multiprocessing
Usando pickle, nem todos os objetos podem ser compartilhados, e erros de objetos que não podem ser serializados com pickle são muito difíceis de depurar em estruturas de dados complexas
Em especial, objetos criados por bibliotecas nativas podem não poder ser compartilhados
Processamentos que precisam compartilhar estado em tempo de execução também são muito difíceis com o módulo multiprocessing; até o exporter Prometheus para Flask precisa de um hack estranho usando um diretório temporário para coletar estatísticas de todos os processos
Em muitas aplicações da DeepMind, eles gostariam de rodar cerca de 50 a 100 threads por processo, mas o GIL frequentemente vira gargalo mesmo com menos de 10
Como solução de contorno, às vezes usam subprocessos, mas em muitos casos o overhead de comunicação entre processos fica grande demais e eles acabam movendo grandes partes da base de código Python para C++
Para usos médios, como apps web, multiprocessing pode ser suficiente, mas em cargas de trabalho de IA em grande escala, como as do Google e da DeepMind, o GIL realmente limita o uso de Python
É por isso que a Meta quer investir três anos-engenheiro nesse trabalho: https://news.ycombinator.com/item?id=36643670
Como o nome sugere, ele só ajuda de verdade em problemas I/O-bound
Compartilhar dados entre vários processos é uma dor enorme, e fazer controle de dados junto com orquestração de processos é uma dor ainda maior
Processos são caros e, pela dificuldade de compartilhamento de dados mencionada acima, greenlets também dificilmente se tornam uma alternativa real
Mas em áreas em que Python tem grande presença, como IA e ciência de dados, poder disparar um monte de threads CPU/GPU-bound e executá-las é uma grande vantagem
Muitos extensões C foram escritos sem levar multithreading em conta, então podem surgir problemas — e é bem provável que surjam
Um pequeno exemplo inseguro se
lstpuder ser acessado por outra thread está aqui: https://news.ycombinator.com/item?id=36649769Mesmo hoje, se código C fizer callback para bytecode Python por meio de um método
__del__e esse bytecode for longo o bastante — talvez cerca de 100 instruções — uma troca de contexto pode acontecerMas é um caso extremamente raro, e muito código de extensão C não foi escrito considerando essa situação
Pessoas que usam extensões C podem estar dependendo do fato de elas serem executadas atomicamente
Por exemplo, colocar e retirar arrays numpy em um pool de threads funciona bem hoje, mas pode quebrar sem o GIL
Por isso a proposta e a direção do trabalho são manter o modo sem GIL totalmente opcional, e não torná-lo padrão
A minoria corajosa que ativar isso precisa estar preparada para gastar um tempo enorme encontrando e corrigindo condições de corrida sutis em décadas de código de bibliotecas Python
Os primeiros adotantes vão sofrer bastante ou, mais provavelmente, restringir o uso do modo sem GIL a processos dedicados muito especializados, com dependências reduzidas ao máximo
É passar de um estado em que se suspeita que há problemas para um estado em que se sabe exatamente onde eles estão; depois é só ir reduzindo essa lista um a um
Dá para adicionar alguma forma de mutex ao redor do código, ou trocar por uma implementação alternativa não nativa que tenha menos chance de causar problemas
O argumento contrário parece ser principalmente que dá muito trabalho, não que seja impossível
Se houver gente suficiente para fazer, resultados podem aparecer
Caso contrário, teriam anunciado um plano de remover o GIL inteiro de uma vez, em vez de uma abordagem gradual em que as pessoas escolhem explicitamente o modo No-GIL
Basta lembrar da transição de texto para Unicode, de 32 bits para 64 bits, da migração da Intel para ARM e do Y2K
No-GIL é uma mudança muito menor e pode seguir o mesmo caminho de transição sem quebrar tudo de forma radical
Mesmo que algo quebre, haverá uma forma bem definida de lidar com esses casos
De qualquer maneira, sobrevivemos a essas transições, e é bom ver o avanço
Isso vai abrir mais áreas que até agora estavam marcadas como impossíveis
Uma das coisas que o Swift inicial fez bem foi colocar mudanças incompatíveis dentro de uma promessa, e todos sabiam onde estavam e se adaptaram bem
Às vezes, gostaria que Python seguisse o mesmo caminho
A passagem de 32 bits para 64 bits, para ARM e o Y2K eram coisas cujo funcionamento podia ser testado
Claro que os testes podem não cobrir casos de falha, mas os testes executados são determinísticos
Aqui, porém, por mais que se teste, a resposta é “está correto, ou você ainda não atingiu a condição de corrida certa”
Eles dizem explicitamente que não querem repetir o cenário da transição para Python 3, mas a abordagem atual parece assustadoramente próxima desse caminho
Muita coisa depende da comunidade Python e dos canais de distribuição
A comunidade pode não adotar a tempo, ou distribuições como Ubuntu, Fedora e Anaconda podem se adiantar cedo demais
Ainda é cedo para afirmar, mas fico em dúvida sobre quanto controle o Steering Council realmente tem para evitar esse tipo de cenário
Cinco anos é tempo demais para Python ter dois modos
Meio decênio é suficiente para que os dois modos se consolidem como o status quo, e posts antigos do Stack Overflow continuarão existindo depois disso
Não estou otimista de que cinco anos não virem dez anos de incerteza e quebras
Por outro lado, cinco anos talvez seja pouco demais para desenterrar todo o código C, corrigi-lo, testá-lo e chamá-lo de maduro
Empresas que prometeram suporte podem converter mecanicamente alguns projetos e incomodar os desenvolvedores reais, ou ameaçá-los com forks
Os bugs serão polidos ao longo de anos pelos desenvolvedores reais não remunerados
Mas Python precisa de algum “sucesso”, e isso vira uma boa frase de divulgação
No mundo Python, exatidão não parece importar muito
Por causa desse histórico, soa como se quisessem manter dois modos de execução dentro do CPython 3, em vez de avançar para outra grande transição major
É bom que estejam muito conscientes de que isso pode facilmente virar um desastre do Python 4
É preciso ter extremo cuidado para não afetar acidentalmente o comportamento com GIL
Se alguma forma de GIL emulado não for exatamente igual ao GIL real, todo tipo de caso estranho será possível
Primeiro, eles não querem que seja assim
Segundo, se for assim, vão desistir rapidamente
As duas são importantes, mas parece faltar a terceira peça essencial: “e é assim que vamos conseguir isso”
Eles já dizem que GIL e No-GIL podem coexistir por mais de 5 anos
Para criadores de ferramentas, isso significa custo dobrado por pelo menos os próximos 5 anos
Porque, seja de forma produtiva ou experimental, as pessoas vão querer usar ferramentas nos dois modos
GIL significa Global Interpreter Lock
Há uma boa explicação aqui: https://realpython.com/python-gil/
Há dois grandes problemas aqui
Primeiro, algumas melhorias valem a quebra de compatibilidade retroativa, e remover o GIL é uma dessas melhorias
É discutível se as mudanças do Python 3 valeram isso, e o fato de
printter virado uma função não parece especialmente valiosoAinda assim, a transição do 2 para o 3 foi em parte exagerada por uma minoria barulhenta, e, pela minha experiência migrando mais de 5 bases de código do 2 para o 3, a maioria teve poucos problemas
O maior problema foi com bases de código que viraram um amontoado de bibliotecas abandonadas porque desenvolvedores anteriores puxaram uma biblioteca para tudo; esse tipo de código tem problemas mesmo que a linguagem central não quebre compatibilidade
A resposta não é pressionar para que a linguagem nunca mais quebre compatibilidade, mas sim não esperar que trazer o pip inteiro seja uma estratégia sustentável
Hoje, o Steering Council recebeu críticas demais de uma minoria barulhenta e passou a temer mudanças incompatíveis
Mas remover o GIL mexe com uma parte tão fundamental de como Python funciona que deveria ser uma mudança incompatível; é melhor admitir isso e planejar a transição do que tentar a tarefa impossível de torná-la não incompatível por medo dos usuários, sem reconhecer os fatos
Minha base de código também quebrou no Python 3.11, e a correção não foi difícil, mas eu gostaria que tivesse havido uma comunicação melhor de que isso poderia acontecer
Segundo, o problema mais fundamental é que muitos outros recursos de Python foram construídos em torno do GIL
Em particular, o paradigma assíncrono faz muito sentido por causa do GIL; sem o GIL, olhando em retrospecto, um modelo de atores no estilo Erlang com send/recv teria sido uma direção muito melhor
É difícil desfazer isso, e parece pouco demais, tarde demais, como se Python estivesse sendo empurrado para um conjunto menos coeso de recursos que não combinam tão bem entre si
Agradeço aos desenvolvedores principais do Python e ao Steering Council; Python é uma das minhas linguagens favoritas, junto com Java e C
A verdadeira multithreading em Python é muito bem-vinda
Uso tanto multiprocessing quanto multithreading, dependendo do projeto; há um exemplo de multiprocessing em [0], e um exemplo de uso de Python Threads em tarefas com muita E/S em [1]
Mas usar threads de verdade seria muito mais eficiente
Threads podem trocar uma quantidade arbitrária de dados em uma única operação atômica e quase instantânea, mas isso não é possível com a interface de loopback local, multiprocessing ou pipes
Estou trabalhando em uma arquitetura multithreading que chamo de “three tier multithreading architecture”
https://github.com/samsquire/three-tier-multithreaded-archit...
O objetivo é um servidor extremamente escalável e performático, mas Python talvez não seja a ferramenta certa para esse trabalho
[0]: https://news.ycombinator.com/item?id=36897054 explicação de uso de multiprocessing
[1]: https://devops-pipeline.com/ exemplo de uso de multithreading