"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Comparando modelos assíncronos Python e ArkScript

Comparando modelos assíncronos Python e ArkScript

Publicado em 2024-11-08
Navegar:258

Comparing Python and ArkScript asynchronous models

Python tem recebido muita atenção ultimamente. O lançamento 3.13, planejado para outubro deste ano, dará início ao enorme trabalho de remoção do GIL. Um pré-lançamento já foi lançado para os usuários curiosos que desejam experimentar um Python (quase) sem GIL.

Todo esse hype me fez pesquisar em minha própria linguagem, ArkScript, já que eu também tinha um Global VM Lock no passado (adicionado na versão 3.0.12, em 2020, removido na 3.1.3 em 2022), para compare as coisas e me force a aprofundar o como e o porquê do Python GIL.

Definições

  1. Para começar, vamos definir o que é um GIL (Bloqueio de intérprete global):

Um bloqueio de interpretador global (GIL) é um mecanismo usado em intérpretes de linguagem de computador para sincronizar a execução de threads para que apenas um thread nativo (por processo) possa executar operações básicas (como alocação de memória e contagem de referência) em um tempo.

Wikipedia - Bloqueio global de intérprete

  1. Simultaneidade é quando duas ou mais tarefas podem ser iniciadas, executadas e concluídas em períodos de tempo sobrepostos, mas isso não significa que ambas serão executadas simultaneamente.

  2. Paralelismo é quando as tarefas são literalmente executadas ao mesmo tempo, por exemplo, em um processador multicore.

Para uma explicação detalhada, verifique esta resposta do Stack Overflow.

GIL de Python

O GIL pode aumentar a velocidade de programas de thread único porque você não precisa adquirir e liberar bloqueios em todas as estruturas de dados: todo o interpretador está bloqueado para que você esteja seguro por padrão.

No entanto, como existe um GIL por intérprete, isso limita o paralelismo: você precisa gerar um intérprete totalmente novo em um processo separado (usando o módulo de multiprocessamento em vez de threading) para usar mais de um núcleo! Isso tem um custo maior do que apenas gerar um novo thread porque agora você precisa se preocupar com a comunicação entre processos, o que adiciona uma sobrecarga não negligenciável (consulte GeekPython — GIL se torna opcional no Python 3.13 para benchmarks).

Como isso afeta o assíncrono do Python?

No caso do Python, isso se deve à implementação principal, CPython, não ter gerenciamento de memória seguro para threads. Sem o GIL, o seguinte cenário geraria uma condição de corrida:

  1. criar uma contagem de variável compartilhada = 5
  2. tópico 1: contagem *= 2
  3. tópico 2: contagem = 1

Se thread 1 for executado primeiro, a contagem será 11 (contagem * 2 = 10, depois contagem 1 = 11).

Se thread 2 for executado primeiro, a contagem será 12 (contagem 1 = 6, depois contagem * 2 = 12).

A ordem de execução é importante, mas pior ainda pode acontecer: se ambos os threads lerem a contagem ao mesmo tempo, um apagará o resultado do outro e a contagem será 10 ou 6!

No geral, ter um GIL torna a implementação (CPython) mais fácil e rápida em casos gerais:

  • mais rápido no caso de thread único (não há necessidade de adquirir/liberar um bloqueio para cada operação)
  • mais rápido no caso multithread para programas vinculados a IO (porque eles acontecem fora do GIL)
  • mais rápido no caso multithread para programas vinculados à CPU que fazem seu trabalho intensivo de computação em C (porque o GIL é liberado antes de chamar o código C)

Também torna mais fácil agrupar bibliotecas C, porque você tem segurança de thread garantida graças ao GIL.

A desvantagem é que seu código é assíncrono como em concorrente, mas não paralelo.

[!OBSERVAÇÃO]
Python 3.13 está removendo o GIL!

O PEP 703 adicionou uma configuração de construção --disable-gil para que ao instalar o Python 3.13 , você possa se beneficiar de melhorias de desempenho em programas multithread.

Modelo Python assíncrono/aguardado

Em Python, as funções precisam ter uma cor: elas são "normais" ou "assíncronas". O que isso significa na prática?

>>> def foo(call_me):
...     print(call_me())
... 
>>> async def a_bar():
...     return 5
... 
>>> def bar():
...     return 6
... 
>>> foo(a_bar)
:2: RuntimeWarning: coroutine 'a_bar' was never awaited
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
>>> foo(bar)
6

Como uma função assíncrona não retorna um valor imediatamente, mas invoca uma corrotina, não podemos usá-las em todos os lugares como retornos de chamada, a menos que a função que estamos chamando seja projetada para receber retornos de chamada assíncronos.

Obtemos uma hierarquia de funções, porque funções "normais" precisam ser assíncronas para usar a palavra-chave await, necessária para chamar funções assíncronas:

         can call
normal -----------> normal

         can call
async - -----------> normal
       |
       .-----------> async                    

Além de confiar no chamador, não há como saber se um retorno de chamada é assíncrono ou não (a menos que você tente chamá-lo primeiro dentro de um bloco try/except para verificar se há uma exceção, mas isso é feio).

Paralelismo ArkScript

No início, ArkScript estava usando um Global VM Lock (semelhante ao GIL do Python), porque o módulo http.arkm (usado para criar servidores HTTP) era multithread e causava problemas com a VM do ArkScript ao alterar seu estado através da modificação de variáveis e chamando funções em vários threads.

Então, em 2021, comecei a trabalhar em um novo modelo para lidar com o estado da VM para que pudéssemos paralelizá-lo facilmente e escrevi um artigo sobre isso. Posteriormente, foi implementado no final de 2021 e o Global VM Lock foi removido.

ArkScript assíncrono/aguarda

ArkScript não atribui uma cor às funções assíncronas, porque elas não existem na linguagem: você tem uma função ou um encerramento, e ambos podem chamar um ao outro sem qualquer sintaxe adicional (um encerramento é um objeto pobre, nesta linguagem: uma função que mantém um estado mutável).

Qualquer função pode ser tornada assíncrona no site de chamada (em vez de declaração):

(let foo (fun (a b c)
    (  a b c)))

(print (foo 1 2 3))  # 6

(let future (async foo 1 2 3))
(print future)          # UserType
(print (await future))  # 6
(print (await future))  # nil

Usando o async embutido, estamos gerando um std::future nos bastidores (aproveitando std::async e threads) para executar nossa função com um conjunto de argumentos. Então podemos chamar await (outro embutido) e obter um resultado sempre que quisermos, o que bloqueará o thread da VM atual até que a função retorne.

Assim, é possível esperar de qualquer função e de qualquer thread.

As especificidades

Tudo isso é possível porque temos uma única VM que opera em um estado contido dentro de um Ark::internal::ExecutionContext, que está vinculado a um único thread. A VM é compartilhada entre os threads, não entre os contextos!

        .---> thread 0, context 0
        |            ^
VM  thread 1, context 1              

Ao criar um futuro usando assíncrono, nós:

  1. copiando todos os argumentos para o novo contexto,
  2. criando uma nova pilha e escopos,
  3. finalmente crie um tópico separado.

Isso proíbe qualquer tipo de sincronização entre threads, pois o ArkScript não expõe referências ou qualquer tipo de bloqueio que possa ser compartilhado (isso foi feito por questões de simplicidade, já que a linguagem pretende ser um tanto minimalista, mas ainda assim utilizável).

No entanto, essa abordagem não é melhor (nem pior) que a do Python, pois criamos um novo thread por chamada e o número de threads por CPU é limitado, o que é um pouco caro. Felizmente, não vejo isso como um problema a ser resolvido, já que nunca se deve criar centenas ou milhares de threads simultaneamente, nem chamar centenas ou milhares de funções assíncronas do Python simultaneamente: ambos resultariam em uma enorme lentidão do seu programa.

No primeiro caso, isso desaceleraria o seu processo (até mesmo o computador), pois o sistema operacional está fazendo malabarismos para dar tempo a cada thread; no segundo caso, é o agendador do Python que teria que fazer malabarismos entre todas as suas corrotinas.

[!OBSERVAÇÃO]
Pronto para uso, o ArkScript não fornece mecanismos para sincronização de threads, mas mesmo se passarmos um UserType (que é um wrapper sobre objetos C apagados por tipo) para uma função, o objeto subjacente é ' não copiei.

Com alguma codificação cuidadosa, seria possível criar um bloqueio usando a construção UserType, que permitiria a sincronização entre threads.

(let lock (module:createLock))
(let foo (fun (lock i) {
  (lock true)
  (print (str:format "hello {}" i))
  (lock false) }))
(async foo lock 1)
(async foo lock 2)

Conclusão

ArkScript e Python usam dois tipos muito diferentes de async/await: o primeiro requer o uso de async no site de chamada e gera um novo thread com seu próprio contexto, enquanto o último requer que o programador marque funções como assíncronas para ser capaz de usar await, e essas funções assíncronas são corrotinas, executadas no mesmo thread que o interpretador.

Fontes

  1. Stack Exchange — Por que Python foi escrito com o GIL?
  2. Python Wiki – GlobalInterpreterLock
  3. stuffwithstuff - Qual é a cor da sua função?

Originalmente de lexp.lt

Declaração de lançamento Este artigo foi reproduzido em: https://dev.to/lexplt/comparing-python-and-arkscript-asynchronous-models-3l60?1 Se houver alguma violação, entre em contato com [email protected] para excluí-la
Tutorial mais recente Mais>

Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.

Copyright© 2022 湘ICP备2022001581号-3