8.1. Concorrência cooperativa

O modelo de escalonamento do asyncio é cooperativo, não preemptivo. Essa distinção é o modelo mental mais importante sobre o qual o restante desta seção se baseia, então vale a pena fixá-la antes que qualquer código apareça.

8.1.1. Preemptivo vs cooperativo

Um escalonador preemptivo – o tipo que um sistema operacional de desktop usa para manter muitos programas em execução ao mesmo tempo – pode pausar qualquer trecho de código em execução em qualquer momento e alternar para outro. O código em execução não precisa fazer nada de especial; o escalonador o interrompe. Isso torna o escalonamento preemptivo muito flexível (nenhum trecho de código pode privar os demais por ser lento), mas também significa que qualquer variável compartilhada precisa ser defendida com cuidado, porque a troca pode acontecer em qualquer ponto – até mesmo no meio da escrita de um valor, ou no meio da leitura de uma lista.

Um escalonador cooperativo só pode alternar entre trechos de código nos pontos em que o trecho em execução devolve o controle explicitamente. No asyncio, esses pontos são cada await e cada chamada a uma corrotina que cede internamente (mais comumente asyncio.sleep()). Entre dois awaits, a corrotina em execução tem a CPU só para si.

Duas consequências surgem disso:

  • Uma corrotina que nunca faz await nunca é pausada. Se uma corrotina fica presa em um loop apertado sem nenhum await dentro, ela monopoliza o escalonador e nada mais progride. A solução é fazer await asyncio.sleep_ms(0) (ou alguma outra chamada de espera) em um ponto sensato do loop.

  • O estado compartilhado é seguro entre awaits. Duas corrotinas não podem se intercalar no meio de uma operação que não tem nenhum await nela. O tipo de corrupção que surge quando a preempção cai no meio de uma atualização de várias etapas – um trecho de código lendo um valor enquanto outro está no meio de sua alteração – simplesmente não pode acontecer aqui. A coordenação entre corrotinas ainda é necessária quando várias delas precisam compartilhar um recurso ao longo de awaits, mas o problema de intercalação no meio de uma linha não se aplica.

8.1.2. As três camadas

Todo script asyncio é construído a partir das mesmas três camadas. As próximas duas páginas as abordam em detalhes; estes são os rótulos a se ter em mente ao lê-las.

  • Corrotinas – funções declaradas com async def, cada uma uma unidade de trabalho autocontida que faz await onde apropriado. A Visão Geral do Python apresentou as palavras-chave async/await; no asyncio, é assim que uma corrotina cede de volta ao escalonador.

  • Tasks – um invólucro que asyncio.create_task() coloca em volta de uma corrotina para escaloná-la concorrentemente com a atual. A aplicação normalmente cria um punhado de tasks para os trabalhos de longa duração (o loop de snapshot, o cliente de rede, o leitor UART, …).

  • O event loop – o motor por baixo que controla quais corrotinas estão esperando e quais estão prontas para executar, alternando entre as tasks a cada await. A aplicação não escreve o loop; ela entrega uma corrotina de nível superior a asyncio.run() e o loop conduz tudo a partir daí.

Quando a aplicação é descrita dessa forma – como um pequeno conjunto de corrotinas compostas por um event loop – a concorrência se torna uma propriedade da forma do programa, não algo que a aplicação precisa gerenciar passo a passo.