8.1. Concorrência cooperativa

O modelo de escalonamento do asyncio é cooperativo, não preemptivo. Esta distinção é o modelo mental mais importante sobre o qual assenta o resto desta secção, pelo que vale a pena fixá-la antes de aparecer qualquer código.

8.1.1. Preemptivo vs. cooperativo

Um escalonador preemptivo — como o que um sistema operativo de secretária utiliza para manter vários programas em execução simultânea — pode interromper o código em execução a qualquer momento e alternar para outro. O código em execução não precisa de fazer nada de especial; o escalonador interrompe-o. Isto torna o escalonamento preemptivo muito flexível (nenhum fragmento de código pode privar os outros por ser lento), mas também significa que qualquer variável partilhada tem de ser protegida com cuidado, porque a mudança pode ocorrer em qualquer ponto — mesmo a meio de uma escrita de valor, ou a meio da leitura de uma lista.

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

Duas consequências decorrem disto:

  • Uma corrotina que nunca aguarda nunca é pausada. Se uma corrotina ficar num ciclo fechado sem nenhum await dentro, monopoliza o escalonador e nada mais progride. A solução é fazer await asyncio.sleep_ms(0) (ou outra chamada de espera) num ponto adequado do ciclo.

  • O estado partilhado é seguro entre awaits. Duas corrotinas não podem intercalar-se a meio de uma operação que não tenha nenhum await. O tipo de corrupção que ocorre quando a preempção acontece a meio de uma atualização com vários passos — um fragmento de código a ler um valor enquanto outro está a meio de o alterar — simplesmente não pode acontecer aqui. A coordenação entre corrotinas ainda é necessária quando várias têm de partilhar um recurso através de awaits, mas o problema de intercalação a meio de uma linha não se aplica.

8.1.2. As três camadas

Todos os scripts asyncio são construídos a partir das mesmas três camadas. As duas páginas seguintes cobrem-nas em detalhe; estes são os rótulos a ter em mente durante a leitura.

  • Corrotinas — funções declaradas com async def, cada uma sendo uma unidade de trabalho autossuficiente que aguarda onde apropriado. A visão geral do Python apresentou as palavras-chave async/await; no asyncio, são a forma de uma corrotina ceder o controlo ao escalonador.

  • Tarefas — um invólucro que asyncio.create_task() coloca à volta de uma corrotina para a escalonar concorrentemente com a atual. A aplicação cria tipicamente um conjunto de tarefas para os trabalhos de longa duração (o ciclo de captura de imagem, o cliente de rede, o leitor UART, …).

  • O ciclo de eventos — o motor subjacente que controla quais corrotinas estão à espera e quais estão prontas a executar, alternando entre tarefas a cada await. A aplicação não escreve o ciclo; entrega uma corrotina de nível superior a asyncio.run() e o ciclo trata de tudo a partir daí.

Quando a aplicação é descrita desta forma — como um pequeno conjunto de corrotinas compostas por um ciclo de eventos — a concorrência torna-se uma propriedade da estrutura do programa, e não algo que a aplicação tem de gerir passo a passo.