1 pontos por GN⁺ 2024-03-12 | 1 comentários | Compartilhar no WhatsApp
  • O PR #116338 do CPython mesclou em python:main uma mudança que permite desativar o GIL em builds free-threaded com PYTHON_GIL=0 ou -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() e drop_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 em test_asyncio
  • Durante a revisão, foram adicionados testes de PYTHON_GIL, documentação, a opção -X gil e o reflexo em sys.flags; o tratamento da configuração também foi corrigido para que PYTHON_GIL=1 force 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
  • colesbury a mesclou em python:main em 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() e drop_gil() retornam antecipadamente
  • Durante a revisão, também foi adicionado um commit que define corretamente enable_gil quando PYTHON_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 vezes
    • x86-64 MacOS Intel ASAN NoGIL PR
    • x86-64 MacOS Intel NoGIL PR
    • ARM64 MacOS M1 Refleaks NoGIL PR
    • ARM64 MacOS M1 NoGIL PR
    • AMD64 Ubuntu NoGIL Refleaks PR
    • AMD64 Ubuntu NoGIL PR
    • AMD64 Windows Server 2022 NoGIL PR

Escopo adicionado durante a revisão

  • corona10 sugeriu que valeria a pena adicionar um teste de variável de ambiente em Lib/test/test_cmd_line.py
  • Depois disso, os seguintes commits foram adicionados
    • Add test for PYTHON_GIL in test_cmd_line
    • Set enable_gil properly when PYTHON_GIL=1
    • Don't add 'enable_gil' to test_embed in normal builds
  • colesbury considerou 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-gil já está documentada
    • Organizou que a documentação deveria incluir que ela só está disponível em builds free-threaded, que 0 força a desativação do GIL, que 1 força a ativação do GIL e que é uma novidade do Python 3.13
  • 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 -X para uso junto com a variável de ambiente
  • O título do PR foi alterado de uma forma que tratava apenas de PYTHON_GIL=0 para incluir também PYTHON_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 option
    • Fix link to -X gil
    • Fix PYTHON_GIL versionchanged line
    • Clarify test_flags in normal builds
  • ericsnowcurrently, erlend-aasland, corona10 e colesbury aprovaram a mudança
  • O commit de merge é 2731913 e, após o merge, vstinner reagiu à mudança dizendo que era “interessante e muito assustadora”

Trabalhos posteriores

  • Duas tarefas foram separadas em issues posteriores
    • #116322: trabalho para reativar o GIL ao carregar extensões incompatíveis
    • #116329: trabalho para desativar o GIL por padrão
  • 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

 
GN⁺ 2024-03-12
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

  • 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

  • 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

    • No futuro, o CPython talvez possa implementar transferência de propriedade, mas é um pouco mais complicado
      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?

    • Antes, por causa do GIL, praticamente não se escrevia Python multithread de verdade. Threads eram usadas principalmente para lidar com várias tarefas que podiam ficar bloqueadas em I/O independente; claro, isso é comum e útil, mas não ajudava no desempenho de código Python centrado em CPU
      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....
    • Se algum pacote depender do GIL, o GIL será ativado. Os pacotes não vão quebrar
  • 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