Voltar ao catálogo
Season 10 20 Episódios 1h 15m 2026

asyncio

v3.14 — Edição de 2026. Uma análise profunda ao framework asyncio do Python, cobrindo o event loop, coroutines, structured concurrency, synchronization primitives e padrões assíncronos avançados. Para Python 3.14.

Python Core Programação Assíncrona
asyncio
A Reproduzir
Click play to start
0:00
0:00
1
O Event Loop e o Modelo Mental
Estabeleça o seu modelo mental base para o asyncio. Aprenda como o event loop atua como um maestro de orquestra, gerindo tarefas de forma cooperativa sem depender de multithreading.
3m 58s
2
Coroutines vs Awaitables
Desmistifique as palavras-chave async e await. Exploramos a distinção crítica entre uma função coroutine e um objeto coroutine, e o que realmente acontece quando faz await de uma operação.
3m 42s
3
O Ponto de Entrada asyncio.run()
Descubra como inicializar uma aplicação asyncio com segurança. Discutimos o asyncio.run, o encerramento de executors e o context manager Runner para ciclos de vida complexos do loop.
3m 47s
4
Agendamento com Tasks
Aprenda a executar operações de forma concorrente usando asyncio.create_task(). Revelamos as graves consequências do garbage collection em tasks não referenciadas.
3m 16s
5
Structured Concurrency com TaskGroups
Domine a structured concurrency. Entenda como o asyncio.TaskGroup gere múltiplas operações concorrentes com segurança e garante encerramentos limpos quando ocorrem exceções.
3m 50s
6
Cancelamento de Tasks e Timeouts
Explore a mecânica de abortar operações. Aprenda por que razão o asyncio.CancelledError é levantado, como lidar com ele num bloco finally e por que nunca o deve ignorar.
4m 03s
7
Ceder o Controlo com Sleep
Entenda o verdadeiro propósito do asyncio.sleep(0). Descubra como ceder o controlo impede que loops intensivos em CPU esgotem o event loop e congelem a aplicação.
3m 30s
8
Sincronização: Locks e Mutexes
Previna race conditions em código async. Exploramos o asyncio.Lock, discutimos a sua natureza não thread-safe e mostramos por que razão os locks de threading congelam o seu event loop.
3m 53s
9
Coordenar Estado com Events
Aprenda a transmitir sinais para múltiplas tasks em espera. Explicamos como o asyncio.Event e o asyncio.Condition substituem de forma elegante loops de polling ineficientes.
3m 49s
10
Limitar Concorrência com Semaphores
Proteja recursos frágeis e evite banimentos por rate-limiting. Descubra como o asyncio.Semaphore limita a execução concorrente sem bloquear a sua arquitetura.
4m 06s
11
Workflows Producer-Consumer
Desacople producers rápidos de consumers lentos com segurança. Explore a asyncio.Queue, a sinalização de conclusão de tasks e as novas mecânicas de encerramento para queues.
3m 37s
12
Networking de Alto Nível com Streams
Mergulhe nas IO Streams de alto nível. Discutimos o StreamReader, o StreamWriter e por que razão omitir o await writer.drain() pode destruir silenciosamente a memória do seu servidor.
3m 38s
13
Construir Servidores Async
Construa servidores de rede altamente concorrentes. Aprenda como o asyncio.start_server abstrai as ligações de clientes, criando uma task isolada para cada peer.
3m 57s
14
Subprocesses Não-bloqueantes
Execute comandos de shell de forma assíncrona. Descubra por que razão usar o módulo subprocess padrão interrompe o event loop, e como o asyncio.create_subprocess_exec resolve isso.
3m 28s
15
Futures: A Ponte de Baixo Nível
Desvende a base das instruções await. Examinamos o asyncio.Future, o seu papel como um resultado eventual e como faz a ponte entre código legado de callbacks e a sintaxe moderna.
3m 45s
16
Transports e Protocols
Vá debaixo do capô para ver como o asyncio comunica com o OS. Entenda a relação 1:1, baseada em callbacks, entre Transports (como os bytes se movem) e Protocols (o que os bytes significam).
4m 26s
17
Threading num Mundo Async
Faça a ponte entre os mundos síncrono e assíncrono. Aprenda a descarregar código bloqueante pesado com segurança usando executors e callbacks thread-safe sem bloquear o loop.
3m 17s
18
Async Generators e Cleanup
Evite fugas de recursos com async generators. Exploramos por que razão a iteração 'async for' pode deixar ligações pendentes quando interrompida, e como o aclosing() oferece segurança.
3m 36s
19
Dominar o Debug Mode
Apanhe bugs de concorrência instantaneamente. Aprenda a usar o PYTHONASYNCIODEBUG para fazer profiling de callbacks lentos, descobrir coroutines sem await e identificar exceções nunca recuperadas.
3m 43s
20
Estender e Custom Loops
O grande final. Exploramos a integração avançada e o que é necessário para escrever um custom event loop ou criar uma subclasse de BaseEventLoop para ambientes especializados e de alta performance.
4m 12s

Episódios

1

O Event Loop e o Modelo Mental

3m 58s

Estabeleça o seu modelo mental base para o asyncio. Aprenda como o event loop atua como um maestro de orquestra, gerindo tarefas de forma cooperativa sem depender de multithreading.

Download
Olá, daqui fala o Alex da DEV STORIES DOT EU. asyncio, episódio 1 de 20. Muitos developers ouvem a palavra assíncrono e assumem que o seu código vai ser executado em paralelo em vários cores de CPU. Mas depois inspecionam a aplicação e descobrem que está a correr inteiramente numa única thread. O segredo desta eficiência sem verdadeiro paralelismo é o event loop, e perceber o seu modelo mental é a base do asyncio. O event loop é o gestor de execução central de qualquer aplicação asyncio. É exatamente o que o nome implica: um loop contínuo que verifica se há operações prontas a correr, executa-as e, depois, procura a operação seguinte. É vital separar este conceito do multithreading. Num programa multithreaded, o sistema operativo controla a execução. O SO vai pausar uma thread à força e mudar para outra para partilhar o tempo de CPU. As próprias threads não têm qualquer controlo sobre quando são pausadas. Isto exige um overhead de sistema significativo para gerir os context switches e proteger a memória partilhada. O event loop opera num modelo completamente diferente chamado cooperative multitasking. Tudo corre sequencialmente numa única thread. O loop nunca interrompe uma operação. Em vez disso, depende do código para ceder explicitamente o controlo de volta ao loop quando tem de esperar por alguma coisa. Pensa no event loop como um único chef experiente numa cozinha movimentada de um restaurante. O chef recebe vários pedidos ao mesmo tempo. Se ele puser uma panela grande de caldo no fogão a ferver em lume brando, não fica parado em frente ao fogão a olhar para o líquido até estar pronto. Essa abordagem ia bloquear a cozinha inteira e mais nada seria cozinhado. Em vez disso, o chef liga o fogão, deixa a panela a ferver em lume brando e vira-se imediatamente para picar legumes para outro prato. O chef representa a única thread de execução. O event loop é o chef a analisar continuamente a cozinha, sabendo exatamente quais as panelas que estão a ferver em lume brando, quais as frigideiras que precisam de ser viradas, e a passar instantaneamente para a próxima tarefa disponível. No teu software, uma panela a ferver em lume brando é geralmente uma operação de input ou output. Quando o teu código envia um request para uma base de dados, a base de dados demora algum tempo a processar a query e a enviar os dados de volta. Um programa síncrono tradicional ia congelar e ficar à espera da resposta. Com um event loop, a operação regista o seu request e depois diz ao loop que está à espera. O event loop muda imediatamente para outro pedaço de código que tem efetivamente dados prontos a processar. Quando a base de dados finalmente responde, a operação original sinaliza ao event loop que está pronta para ser retomada. O event loop coloca-a de volta na queue e vai retomar a sua execução assim que a tarefa atual fizer yield. Aqui está o ponto-chave. Como o event loop não pode parar uma operação à força, todo o sistema depende inteiramente da cooperação. Se uma tarefa decidir fazer um cálculo matemático enorme sem nunca ceder o controlo, o event loop para. A única thread fica ocupada. Na nossa cozinha, isto é o chef a decidir moer manualmente um saco enorme de farinha enquanto ignora todos os outros pratos. As panelas a ferver em lume brando transbordam, novos pedidos acumulam-se e a cozinha para completamente. O loop é apenas tão eficiente quanto o código que corre dentro dele. A verdadeira eficiência assíncrona não vem de fazer múltiplos cálculos exatamente no mesmo momento físico, mas sim de garantir que a tua única thread nunca desperdiça um único milissegundo em idle enquanto espera pelo mundo exterior. Se quiseres ajudar a manter o programa no ar, podes apoiar-nos ao procurar por DevStoriesEU no Patreon. Obrigado por ouvirem, happy coding a todos!
2

Coroutines vs Awaitables

3m 42s

Desmistifique as palavras-chave async e await. Exploramos a distinção crítica entre uma função coroutine e um objeto coroutine, e o que realmente acontece quando faz await de uma operação.

Download
Olá, daqui é o Alex do DEV STORIES DOT EU. asyncio, episódio 2 de 20. Escreves uma função, chamas a função, e absolutamente nada acontece. O teu código corre sem erros, mas a base de dados está vazia e o request de rede nunca dispara. O problema é um mal-entendido fundamental sobre o que chamar uma função assíncrona realmente faz. Hoje, vamos olhar para Coroutines versus Awaitables. Em Python normal, quando chamas uma função standard, ela corre imediatamente. As funções assíncronas quebram esta regra por completo. Há uma diferença estrita entre uma coroutine function e um coroutine object. Quando escreves async def, estás a criar uma coroutine function. Quando chamas essa função no teu código, ela não executa o corpo da função. Em vez disso, devolve um coroutine object. Pensa nisto como pedir um café. A função async def é o item do menu. Chamar essa função é como fazer o teu pedido na caixa. Recebes um recibo. Esse recibo é o teu coroutine object. Já declaraste a tua intenção, mas ainda não tens a tua bebida, e ninguém sequer a começou a fazer. Para realmente disparar o processo de preparação e receberes o teu café, tens de esperar ao balcão. Em Python, fazes isto usando a keyword await. Quando escreves await seguido desse coroutine object, acontecem duas coisas distintas. Primeiro, a coroutine finalmente começa a executar o seu código interno. Segundo, a função onde colocaste o await pausa por completo. Ela devolve o controlo ao Python, indicando que não pode prosseguir até que esta coroutine específica termine. Este comportamento de pausa é a principal diferença mecânica da programação assíncrona. Enquanto a tua função está pausada à espera do café, o Python fica livre para ir correr outro código noutro lado. Isto leva-nos ao termo mais abrangente, awaitable. Um awaitable é simplesmente qualquer objeto que o Python te permite usar com a keyword await. Todas as coroutines são awaitables. Quando vires await, lê isso como um comando direto: corre este objeto awaitable até ao fim, e suspende o meu progresso atual até que ele devolva um resultado final. Se escreveres uma função async chamada fetch data, simplesmente chamar fetch data devolve o coroutine object. Se atribuíres essa chamada a uma variável chamada pending request, essa variável apenas guarda a coroutine não executada. A rede permanece completamente silenciosa. Mais tarde no teu script, quando escreveres await pending request, o Python finalmente executa a chamada de rede. A execução do teu bloco de código atual para exatamente nessa linha. Assim que o servidor responder, a expressão await resolve-se nos dados devolvidos, e o teu código em redor continua para a linha seguinte. Aqui está o ponto-chave. Só podes usar a keyword await dentro de uma função async def. Como fazer await a um objeto requer pausar a execução atual, a função que contém o await tem de ser ela própria capaz de ser pausada. É por isso que o comportamento assíncrono se propaga para fora. Para fazeres await de uma coroutine, tens de estar dentro de uma coroutine. Estás a construir uma chain de operações suspensas, todas à espera que a task de nível mais baixo se resolva. Lembra-te, chamar uma função async sem fazeres await é apenas gerar um recibo para um trabalho que nunca pediste realmente a ninguém para fazer. O código nunca vai correr até que faças await. Obrigado por ouvires. Até à próxima!
3

O Ponto de Entrada asyncio.run()

3m 47s

Descubra como inicializar uma aplicação asyncio com segurança. Discutimos o asyncio.run, o encerramento de executors e o context manager Runner para ciclos de vida complexos do loop.

Download
Olá, daqui fala o Alex da DEV STORIES DOT EU. asyncio, episódio 3 de 20. O uso incorreto do ponto de partida da tua aplicação assíncrona pode deixar para trás dangling thread executors e async generators por fechar. Para evitar resource leaks ocultos, precisas de usar a ferramenta certa para iniciar e parar a tua aplicação, o que nos leva ao entry point asyncio.run. Muitos developers tentam usar esta ferramenta, erradamente, para executar coroutines individuais de forma aleatória a partir de código síncrono. Não é esse o seu propósito. Não podes chamar a função run quando outro event loop do asyncio já estiver a correr exatamente na mesma thread. Fazer isso gera imediatamente um runtime error. Ela foi concebida especificamente para ser o entry point único e de alto nível de um programa. Pensa na inicialização do main loop de um web server que coordena todos os requests de tráfego recebidos. Tens uma função assíncrona central que faz bind a uma porta de rede, configura os request handlers e mantém o servidor vivo. Passas essa única função principal para a função run. Quando fazes isto, o asyncio gere automaticamente todo o ciclo de vida do event loop. Primeiro, cria um novo event loop e define-o como o loop ativo atual para a thread. A seguir, executa a coroutine principal do teu web server até estar concluída. Aqui está o ponto chave. O trabalho mais valioso que esta função faz acontece depois de o teu código principal terminar a execução. Ela faz um cleanup rigoroso. Antes de devolver o controlo à parte síncrona do teu programa, ela cancela quaisquer pending tasks que tenham sobrado. Depois, encerra em segurança as background threads no default executor. Finalmente, finaliza todos os async generators antes de fechar completamente o event loop. Também podes passar uma debug flag a esta função, o que força o loop subjacente a correr em debug mode para ajudar a rastrear problemas de execução. Como esta função standard faz o teardown de tudo no fim, cria uma fronteira rígida. Se tiveres um cenário onde precisas de correr vários blocos assíncronos distintos a partir de código síncrono, mas queres que eles partilhem o mesmo event loop, chamar a função run standard consecutivamente vai falhar, porque um novo loop é criado e destruído todas as vezes. Para essa situação, usas o context manager Runner do asyncio. Abres um bloco de contexto usando a instrução with standard do Python. Entrar neste bloco inicializa o event loop. Uma vez lá dentro, podes chamar o método run do próprio objeto runner. Passas-lhe uma coroutine, ele corre-a até ao fim e devolve o resultado. Podes chamar este método run interno várias vezes dentro do mesmo bloco de contexto. O event loop permanece vivo, mantendo o estado, os dados em cache e as ligações entre essas chamadas separadas. Podes configurar o context manager ao criá-lo, passando uma debug flag, ou até mesmo uma custom loop factory, caso o teu ambiente exija uma implementação especializada do event loop. Quando a execução finalmente sai do bloco do context manager, o runner executa exatamente a mesma teardown sequence que a standalone function. Ele limpa os executors, finaliza os generators e fecha o loop em segurança. A estabilidade da tua aplicação depende inteiramente de como ela começa e acaba. Quer uses uma única chamada de função ou o context manager, encaminhar a tua execução através destes entry points oficiais é a única maneira de garantir que os teus recursos assíncronos são encerrados de forma fiável quando o programa termina. É tudo por este episódio. Obrigado por ouvires, e continua a desenvolver!
4

Agendamento com Tasks

3m 16s

Aprenda a executar operações de forma concorrente usando asyncio.create_task(). Revelamos as graves consequências do garbage collection em tasks não referenciadas.

Download
Olá, daqui é o Alex da DEV STORIES DOT EU. asyncio, episódio 4 de 20. Inicias um background process para enviar métricas do sistema. Mais tarde, vais verificar o teu dashboard e percebes que metade dos dados está a faltar. Não foram lançados erros. O teu código simplesmente parou silenciosamente a meio da execução. Isto acontece porque trataste o teu background job como fire-and-forget. Hoje, vamos falar sobre scheduling com Tasks, e por que razão deves sempre guardar as coisas que crias. Quando tens uma coroutine que queres correr de forma concorrente com outro código, usas a função create task do asyncio. Passas a tua coroutine para esta função, e o asyncio encapsula-a num objeto Task. Isto diz ao event loop para agendar a task para execução. A função devolve-te imediatamente o novo objeto Task, permitindo que o teu programa principal continue a correr enquanto a task opera em background. Muitos developers chamam o create task e ignoram o return value. Isto é uma armadilha gigante. Aqui está o ponto chave. O event loop do asyncio mantém apenas weak references para as tasks que está a correr. O próprio loop não protege a tua task do garbage collector do Python. Se não atribuíres o objeto Task devolvido a uma variável, ou se não o guardares numa estrutura de dados, o garbage collector vai acabar por notar que não existem hard references. Quando isso acontece, o Python destrói o objeto Task. Ele não quer saber se a coroutine está mesmo a meio da execução de uma database query ou à espera de uma network response. A task simplesmente desaparece. Pensa numa função async chamada ship metrics. Ela formata um data payload e envia um HTTP request para um servidor externo. Chamas o create task e passas a ship metrics, mas não atribuis o resultado a nada. A task começa a correr. Ela formata o payload. Depois, chega à network call e faz uma pausa à espera de uma ligação. Enquanto está em pausa, o garbage collector corre. O strong reference count é zero. A task é destruída. O servidor nunca recebe o payload, e a tua aplicação nunca faz log de um erro porque a execução simplesmente deixou de existir. Para evitar isto, deves sempre manter uma strong reference para as tasks que agendas. Se estiveres a criar uma única task, atribui-a a uma variável. Se estiveres a agendar várias background tasks dentro de um loop, adiciona-as a um set normal do Python ou a uma list. Enquanto esse set existir em memória, as strong references existem, e o garbage collector vai deixar as tuas tasks a correr em paz. Podes então usar um callback para remover a task do teu set assim que ela estiver concluída. A função create task também aceita alguns argumentos opcionais. Podes passar uma string para o parâmetro name, que atribui um identificador específico à task. Isto é altamente recomendado para debugging, pois facilita muito descobrir que operação específica falhou caso uma exceção seja lançada mais tarde. Também podes passar um argumento context para estabelecer um estado de context variable específico para a task. Tratar background operations como fire-and-forget vai acabar por te tramar com silent failures. Se pedires ao event loop para correr alguma coisa, tens de manter uma hard reference para o objeto resultante até que o trabalho esteja completamente terminado. Obrigado por ouvirem, happy coding a todos!
5

Structured Concurrency com TaskGroups

3m 50s

Domine a structured concurrency. Entenda como o asyncio.TaskGroup gere múltiplas operações concorrentes com segurança e garante encerramentos limpos quando ocorrem exceções.

Download
Olá, daqui fala o Alex do DEV STORIES DOT EU. asyncio, episódio 5 de 20. Antes do Python 3.11, lançar várias tasks concorrentes era fácil, mas lidar com elas de forma segura quando uma falhava era notoriamente difícil. Muitas vezes, acabavas com background tasks órfãs a desperdiçar recursos silenciosamente. A solução para esta confusão é a concorrência estruturada usando o TaskGroup. Um TaskGroup é um context manager assíncrono. Às vezes, as pessoas confundem-no com uma lista normal de tasks, mas é muito mais rigoroso. Oferece fortes garantias de segurança sobre como as tasks começam e terminam. Impõe a regra de que uma rotina pai não pode terminar até que todas as suas operações filhas estejam concluídas ou sejam canceladas de forma limpa. Usas isto abrindo um bloco async with. Dentro desse bloco, chamas o método create task diretamente no objeto do grupo para iniciar as tuas operações concorrentes. Não despejas estas tasks num array normal e fazes await delas manualmente. Em vez disso, quando o código chega ao fim do bloco async with, o TaskGroup faz uma pausa implícita. Fica ali à espera até que todas as tasks lançadas terminem. O bloco simplesmente não sai mais cedo. Aqui está o ponto chave. O verdadeiro poder de um TaskGroup está na forma como lida com falhas. Com ferramentas legacy como o gather, se iniciasses várias tasks e uma delas lançasse um erro, as outras continuavam a correr em background. Tinhas de escrever uma lógica complexa de error handling para rastrear as sobreviventes e matá-las. Um TaskGroup lida com isto automaticamente. Imagina o cenário de um web scraper a consultar três endpoints de API distintos em simultâneo. Precisas de dados de utilizadores, posts recentes e alertas do sistema. Abres um TaskGroup e lanças três tasks. Todas começam a correr de forma concorrente pela rede. A meio da operação, o endpoint dos posts recentes dá timeout e lança um erro de ligação. O TaskGroup deteta imediatamente esta falha. Interceta o erro e envia automaticamente um sinal de cancelamento para a task dos dados de utilizadores e para a task dos alertas do sistema. Limpa essas operações pendentes para que não continuem a consumir largura de banda da rede ou memória. As tasks restantes lançam um erro de cancelamento internamente, reconhecendo o encerramento. Assim que todas as tasks restantes estiverem paradas em segurança, o TaskGroup agrupa o erro de ligação original numa nova estrutura chamada ExceptionGroup, e lança-o para fora do bloco de contexto. Este comportamento torna o teu código assíncrono totalmente previsível. Se a execução passar do bloco com sucesso, sabes de certeza que todas as tasks foram bem-sucedidas. Se o bloco lançar um ExceptionGroup, sabes que a falha foi apanhada e que tudo o resto foi encerrado corretamente. Nunca deixas tasks descontroladas a correr em background. Se precisares dos resultados das tasks bem-sucedidas, podes recuperá-los diretamente dos objetos das tasks que criaste, desde que tenham terminado antes de a falha ocorrer. Ao vincular as tasks a um bloco de ciclo de vida estrito, os TaskGroups garantem que as operações concorrentes entram e saem da tua aplicação como uma única unidade coordenada. É tudo por hoje. Obrigado por ouvires — vai construir algo fixe.
6

Cancelamento de Tasks e Timeouts

4m 03s

Explore a mecânica de abortar operações. Aprenda por que razão o asyncio.CancelledError é levantado, como lidar com ele num bloco finally e por que nunca o deve ignorar.

Download
Olá, daqui é o Alex da DEV STORIES DOT EU. asyncio, episódio 6 de 20. Escreveste um error handler robusto, que apanha todas as exceptions genéricas no teu async worker. Mas agora, durante o shutdown, o teu event loop fica entupido com zombie tasks que se recusam a morrer. A tua rede de segurança está, na verdade, a prendê-las vivas. É exatamente isso que vamos abordar hoje: Task Cancellation e Timeouts. Quando precisas de parar uma task a correr, chamas o seu método cancel. Isto não termina a task instantaneamente como matar um processo do sistema. Em vez disso, o asyncio pede para parar ao injetar um erro, especificamente um asyncio CancelledError, na task. Este erro é lançado exatamente no ponto de await atual ou seguinte da task. A coroutine depois faz o unwind da sua stack, tal como faria para qualquer erro standard de Python. Este mecanismo também é a base para os timeouts. Quando fazes wrap de uma task numa função de timeout e o timer expira, o event loop não interrompe a task por magia. Ele simplesmente chama cancel nessa task. A task recebe o CancelledError no seu próximo await, faz o unwind do seu state e, eventualmente, diz ao timeout wrapper que parou. Só então é que o timeout wrapper faz raise de um TimeoutError de volta para ti. Aqui está o ponto chave. Desde o Python 3.8, o CancelledError herda diretamente de BaseException, e não da classe Exception standard. Esta escolha de design evita um erro específico e catastrófico. Os developers costumam fazer wrap de operações de rede ou de ficheiros em blocos try e except que apanham classes Exception genéricas para evitar um crash. Se o CancelledError fosse uma Exception standard, esses blocos iriam apanhar o sinal de cancelamento. A task talvez fizesse log de um warning, engolisse o sinal e continuasse a executar como um zombie. Ao mover o CancelledError para cima na hierarquia para a BaseException, o Python garante que os teus error handlers do dia a dia não intercetam acidentalmente um pedido de cancelamento. Então, como geres o state com segurança quando uma task é cancelada? Dependes da estrutura try e finally. Considera um web server a processar um HTTP request recebido. O utilizador pede um report massivo, mas depois fecha a janela do browser. O server deteta o disconnect e cancela a task do request. Dentro do teu código, estás neste momento a fazer await de uma database query demorada. Esse await lança de repente um CancelledError. Como colocaste a tua interação com a base de dados dentro de um bloco try, a execução salta instantaneamente para o teu bloco finally. Usas esse bloco finally para fazer um rollback limpo da transação pendente e devolver a database connection ao pool. Assim que o bloco finally termina, o CancelledError continua a fazer bubble up, terminando a task com sucesso. Às vezes, um bloco finally não é suficiente. Se precisares mesmo de fazer um cleanup assíncrono, como enviar um network request para um microservice remoto para anunciar o abort, podes apanhar explicitamente o CancelledError. Mas se fizeres isto, tens de fazer re-raise explícito desse mesmo erro no final do teu bloco except. Falhar esse re-raise quebra a mecânica interna do asyncio. A task vai parecer ter terminado com sucesso em vez de ter sido cancelada, o que corrompe o state da tua aplicação e quebra a structured concurrency. A regra a lembrar é que o cancellation é um pedido cooperativo, não um comando kill forçado, e depende inteiramente das exceptions a fazerem bubble up intocadas. Se quiseres apoiar o programa, podes procurar por DevStoriesEU no Patreon. É tudo por este episódio. Obrigado por ouvires, e continua a programar!
7

Ceder o Controlo com Sleep

3m 30s

Entenda o verdadeiro propósito do asyncio.sleep(0). Descubra como ceder o controlo impede que loops intensivos em CPU esgotem o event loop e congelem a aplicação.

Download
Olá, daqui é o Alex da DEV STORIES DOT EU. asyncio, episódio 7 de 20. Às vezes, o segredo para manteres o teu servidor de rede responsivo é dizeres às tuas tasks mais pesadas para fazerem sleep durante exatamente zero segundos. Se uma função nunca pausa, toda a tua aplicação deixa de ouvir o mundo exterior. Para corrigir isto, fazes yield do controlo com o sleep. Na framework asyncio, o event loop corre exatamente uma task de cada vez. Depende inteiramente de multitasking cooperativo. Uma task corre continuamente até encontrar uma keyword await, que atua como um checkpoint para devolver o controlo de execução ao loop. Se escreveres uma função async que contenha uma operação puramente CPU-bound, crias um bottleneck. Pensa em fazer o parsing de um payload JSON enorme ou transformar milhares de strings. Não existem pontos de await naturais num loop standard de processamento de dados. Como a task nunca faz yield, o event loop permanece bloqueado. Quaisquer requests de rede a chegar, respostas da base de dados ou health checks ficam simplesmente numa queue, em starvation, enquanto esperam que o teu loop termine. A forma nativa de resolver isto é devolveres manualmente o controlo ao event loop. Fazes isto usando um padrão específico: fazer await de asyncio dot sleep com um argumento de zero. À primeira vista, fazer sleep durante zero segundos parece uma operação inútil. Porquê pedir ao sistema para não esperar tempo nenhum? Aqui está o ponto chave. Um sleep de zero segundos não tem a ver com a passagem do tempo. É um sinal explícito para o event loop. Quando fazes await de um sleep de zero, a coroutine atual é imediatamente suspensa. O event loop assume o controlo, coloca a tua task suspensa no fim da queue de runnables, e verifica se outras tasks agendadas estão prontas para executar. Se um handler de rede em background estiver à espera para confirmar uma ligação de entrada, ele tem a sua vez. Assim que as outras tasks atingirem os seus próprios pontos de await ou terminarem, a tua task original volta à frente da queue e retoma exatamente onde parou. Vamos aplicar isto a um cenário concreto. Estás a escrever uma função async para processar milhões de registos de um ficheiro JSON. Se correres um while loop de seguida, o teu servidor parece morto. Em vez disso, introduzes uma variável de contador. Dentro do loop, processas um registo e incrementas o contador. Depois, adicionas uma condição simples. Se o contador indicar que passaram cem iterações, fazes await de asyncio dot sleep zero. Isto divide a computação massiva em chunks geríveis. O loop processa cem registos, afasta-se para deixar o servidor responder a pings ou aceitar novos dados, e depois retoma o parsing dos próximos cem. O número de iterações entre yields é um parâmetro que tens de afinar. Fazer yield a cada iteração adiciona demasiado overhead, porque suspender e retomar uma coroutine tem um pequeno custo computacional. Fazer yield a cada dez mil iterações ainda pode bloquear o event loop durante demasiado tempo. Cem é um ponto de partida razoável para manter o loop a respirar. Forçar um sleep de zero segundos é a forma mais simples de manter a tua aplicação cooperativa, garantindo que um único loop pesado nunca cause starvation ao resto do teu sistema. Obrigado por ouvires, happy coding a todos!
8

Sincronização: Locks e Mutexes

3m 53s

Previna race conditions em código async. Exploramos o asyncio.Lock, discutimos a sua natureza não thread-safe e mostramos por que razão os locks de threading congelam o seu event loop.

Download
Olá, daqui é o Alex do DEV STORIES DOT EU. asyncio, episódio 8 de 20. Largas um lock de threading standard na tua aplicação async para proteger um recurso partilhado, e de repente todo o teu event loop congela completamente. O lock cumpriu a sua função, mas parou tudo o resto no processo. Para resolver isto sem bloquear o loop, usamos Sincronização: Locks e Mutexes. Um Lock do asyncio, frequentemente chamado de mutex, garante acesso exclusivo a um recurso partilhado entre tasks assíncronas. Primeiro, precisamos de esclarecer uma confusão comum. Não podes usar um thread lock standard do módulo threading do Python dentro de uma aplicação async. Um threading lock opera ao nível do sistema operativo. Se não conseguir adquirir o lock, pausa a thread inteira. Como o asyncio corre várias tasks cooperativamente numa única thread, bloquear essa thread significa que o event loop para. Nenhum request de rede é disparado, nenhum timer avança. Tudo congela. Um lock do asyncio resolve isto por ser task-safe, não thread-safe. Quando uma task do asyncio tenta adquirir um mutex bloqueado, não bloqueia a thread. Em vez disso, suspende-se e devolve o controlo ao event loop. Isto permite que outras tasks não relacionadas continuem o seu trabalho enquanto a primeira task espera na fila. Vamos ilustrar isto com um cenário concreto. Tens uma aplicação com dezenas de tasks async a fazer chamadas a APIs externas. O teu token OAuth expira. Duas tasks diferentes detetam o token expirado exatamente no mesmo milissegundo. Sem sincronização, ambas as tasks vão disparar de forma independente um request para o servidor de autenticação para fazer o refresh do token. Este trabalho redundante pode disparar rate limits ou invalidar imediatamente o primeiro token devido a políticas de rotação rigorosas. Para evitar esta race condition, crias um único lock do asyncio quando inicializas a tua aplicação. Este objeto lock é passado ou partilhado entre todas as tuas tasks de API. Agora, repara no fluxo. A Task A e a Task B detetam ambas o token expirado. A Task A chega primeiro ao bloco de sincronização e faz await do lock. E adquire-o com sucesso. A Task B chega uma fração de segundo depois e faz await do mesmo lock. Como a Task A o detém, a Task B vai dormir, deixando o event loop tratar de outras tarefas. Quando várias tasks esperam pelo mesmo lock, o asyncio coloca-as na fila. Assim que o lock é libertado, o event loop acorda a primeira task da fila. A Task A pede o novo token de forma segura, atualiza a variável partilhada do token, e liberta o lock. Nesse momento, o event loop acorda a Task B. A Task B finalmente adquire o lock. No entanto, antes de fazer uma chamada de rede, a Task B verifica o token novamente. Vê que o token já é válido, salta o passo de refresh, liberta o lock, e continua com o seu request principal de API. A maneira mais segura de implementar esta lógica é usando um context manager assíncrono. No teu código, escreves um statement async with seguido pelo objeto lock. Quando a execução entra neste bloco, espera por acesso exclusivo. Quando a execução sai do bloco, seja normalmente ou porque um erro fez crashar a task, liberta automaticamente o lock. Não precisas de chamar manualmente os métodos acquire ou release, o que elimina o risco de deixares acidentalmente um lock preso para sempre. Aqui está o ponto-chave. Um lock do asyncio não protege o teu estado de outras threads do sistema operativo; protege o teu estado das tuas próprias tasks concorrentes, para não se atropelarem umas às outras enquanto fazem await de outras operações. Obrigado por estares aí. Espero que tenhas aprendido algo novo.
9

Coordenar Estado com Events

3m 49s

Aprenda a transmitir sinais para múltiplas tasks em espera. Explicamos como o asyncio.Event e o asyncio.Condition substituem de forma elegante loops de polling ineficientes.

Download
Olá, daqui é o Alex do DEV STORIES DOT EU. asyncio, episódio 9 de 20. Tens cinquenta tasks à espera que uma base de dados se ligue. Definitivamente não queres que elas fiquem a fazer polling num loop, a desperdiçar ciclos de CPU enquanto verificam se a ligação está pronta. Precisas de um único sinal de broadcast que lhes diga a todas para começarem a fazer queries exatamente no mesmo instante. É exatamente isto que a coordenação de state com Events e Conditions resolve. Um Event do asyncio gere uma simples flag booleana interna. Ela começa como false. Antes de olharmos para o flow, vamos esclarecer uma confusão comum entre Events e Locks. Um Lock concede acesso exclusivo a exatamente uma task de cada vez, mantendo as outras de fora. Um Event faz o oposto. Notifica várias tasks em espera em simultâneo, permitindo que todas avancem ao mesmo tempo. Pensa naquele cenário da ligação à base de dados. A tua background task está a trabalhar para estabelecer a ligação. Entretanto, as tuas cinquenta worker tasks chegam a um ponto em que precisam da base de dados. Cada worker chama o método wait no teu objeto Event partilhado. Como a flag interna é false, todas as cinquenta tasks ficam suspensas. Ficam paradas. Eventualmente, a background task tem sucesso e chama o método set no Event. A flag passa a true. Instantaneamente, todas as cinquenta worker tasks suspensas acordam e retomam a execução. Se precisares de fechar a ligação mais tarde, podes chamar o método clear no Event. A flag volta a false, e quaisquer futuras chamadas ao wait vão bloquear novamente. Também podes verificar o status atual da flag a qualquer momento chamando o método is set, que devolve true ou false sem bloquear a task. Isto cobre sinais de broadcast simples. Às vezes, uma única flag booleana não é suficiente. Podes ter várias tasks que precisam de esperar que um recurso partilhado atinja um state complexo específico, e precisam de acesso exclusivo para verificar ou modificar esse state com segurança. É aqui que entra a Condition do asyncio. Uma Condition é construída à volta de um Lock subjacente. Para fazer qualquer coisa com uma Condition, uma task tem primeiro de a adquirir. Uma vez adquirida, a task verifica o state partilhado. Se o state não for o que a task precisa, a task chama o método wait na Condition. Aqui está o ponto chave. Chamar o wait numa Condition faz duas coisas ao mesmo tempo: liberta o Lock subjacente, permitindo que outras tasks acedam ao state, e suspende a task atual. Enquanto essa task está suspensa, outra task pode adquirir a Condition, alterar o state partilhado, e depois chamar o método notify. O método notify recebe um argumento que especifica exatamente quantas tasks em espera deve acordar, sendo o default uma. Também podes chamar o notify all para acordar todas de uma só vez. Quando uma task suspensa acorda, não corre logo imediatamente. Tem de esperar para readquirir o Lock subjacente antes que o método wait retorne. Como outra task pode apanhar o Lock e alterar o state antes que a task acordada tenha a sua vez, a chamada ao wait é quase sempre colocada dentro de um while loop que verifica continuamente o state desejado. Assim que tiver o Lock de volta e o state estiver correto, pode prosseguir com segurança e, eventualmente, libertar a Condition. Ao decidir entre os dois, lembra-te que um Event é um simples broadcast a dizer às tasks que uma ação pontual aconteceu, enquanto uma Condition permite que as tasks esperem com segurança por uma mudança de state complexa sem estarem constantemente a fazer polling a um recurso bloqueado. Obrigado por passares uns minutos comigo. Até à próxima, fica bem.
10

Limitar Concorrência com Semaphores

4m 06s

Proteja recursos frágeis e evite banimentos por rate-limiting. Descubra como o asyncio.Semaphore limita a execução concorrente sem bloquear a sua arquitetura.

Download
Olá, daqui é o Alex do DEV STORIES DOT EU. asyncio, episódio 10 de 20. Disparar dez mil requests assíncronos para uma API de terceiros frágil é uma forma altamente eficiente de teres o teu endereço IP banido permanentemente. O teu código corre na perfeição, mas o servidor do outro lado entra em colapso com o pico repentino de tráfego. Para protegeres serviços externos e o teu próprio acesso, tens de fazer throttle à tua aplicação. Esse escudo é a Limitação de Concorrência com Semaphores. Convém esclarecer um equívoco comum logo à partida. Um semaphore não é um rate limiter. Não limita quantos requests o teu programa faz por segundo. Em vez disso, limita operações concorrentes. Controla rigorosamente quantas tasks podem executar um bloco específico de operações de rede ou de ficheiros exatamente no mesmo instante. Se uma task terminar a sua chamada à API em dez milissegundos, esse slot abre-se imediatamente para a próxima task na fila. Ainda podes processar centenas de operações por segundo, desde que não haja mais do que o limite permitido a decorrer em simultâneo. Um Semaphore do asyncio gere um contador interno simples. Quando crias o objeto semaphore, forneces um valor inicial. Imagina o cenário de limitar os requests HTTP de saída para uma API externa sensível a exatamente dez ligações concorrentes. Inicializas o teu semaphore com o valor de dez. Antes de qualquer task assíncrona fazer um request de rede, tem de fazer acquire do semaphore. Esta ação diminui o contador interno em um. Quando o request de rede termina, a task faz release do semaphore, voltando a aumentar o contador em um. Aqui está o ponto chave. Se dez tasks já fizeram acquire do semaphore, o contador fica a zero. Quando a décima primeira task tenta fazer acquire, essa task é suspensa. O método acquire bloqueia o progresso até que uma das dez primeiras tasks termine e faça release. Este simples lock numérico garante que nunca excedes o teu hard limit de dez ligações ativas. Na prática, raramente deves chamar os métodos acquire e release manualmente. Em vez disso, usas o semaphore como um context manager assíncrono. Ao envolveres o teu request HTTP num statement with assíncrono, o Python garante que é feito o release do semaphore quando o bloco de código termina. Este release acontece mesmo se a API der timeout, deixar cair a ligação, ou lançar uma exceção não tratada. Se tentares fazer releases manuais e um erro fizer saltar a tua chamada de release, esse slot de concorrência perde-se para sempre. Se perderes todos os dez slots para erros de rede transitórios, todo o teu programa entra em deadlock silenciosamente. Existe um perigo subtil com o semaphore standard. Se um erro de lógica no teu código fizer com que uma task faça release do semaphore mais vezes do que fez acquire, o contador interno vai aumentar para lá do teu limite original de dez. De repente, o teu escudo de concorrência quebra-se, e estás, sem saberes, a enviar doze ou quinze requests em simultâneo. Para evitar isto, deves usar um Bounded Semaphore do asyncio. Um Bounded Semaphore comporta-se exatamente como um semaphore standard, mas monitoriza o valor inicial que lhe deste. Se uma task descontrolada tentar fazer release do semaphore para lá desse limite inicial, o Bounded Semaphore levanta imediatamente um ValueError. Faz crash cedo e de forma ruidosa, em vez de sobrecarregar silenciosamente a API externa. Usa sempre um Bounded Semaphore por defeito, a não ser que tenhas um motivo arquitetónico muito específico para inflacionar os teus limites de concorrência dinamicamente. Os Bounded Semaphores apanham erros lógicos de release no momento em que acontecem, mantendo os limites de ligação da tua API estritos e os teus sistemas a correr de forma previsível. É tudo por este episódio. Obrigado por ouvires, e continua a programar!
11

Workflows Producer-Consumer

3m 37s

Desacople producers rápidos de consumers lentos com segurança. Explore a asyncio.Queue, a sinalização de conclusão de tasks e as novas mecânicas de encerramento para queues.

Download
Olá, daqui fala o Alex do DEV STORIES DOT EU. asyncio, episódio 11 de 20. Tens um web server async a lidar com milhares de requests por segundo e, por cada request, precisas de gravar um log no disco. Se o teu servidor esperar que essa gravação no disco termine antes de responder, a tua performance colapsa. A maneira mais fiável de desacoplar producers rápidos de consumers lentos em Python async já vem integrada na standard library. Hoje vamos olhar para Producer-Consumer Workflows usando queues do asyncio. Alguns developers que vêm da programação multi-threaded assumem que precisam de envolver esta queue em locks para evitar race conditions. Não precisas. A queue do asyncio foi desenhada especificamente para tasks concorrentes a correr num único event loop. Ela é inerentemente segura para essas tasks. Deixa as queues thread-safe do módulo queue da standard library para threading; usa a versão do asyncio para async. Pensa na queue como um pipe. Numa ponta, tens producers a fazer push de itens. Na outra ponta, tens consumers a fazer pull de itens. Vamos usar aquele cenário de logging. O teu web request handler é o producer. Ele recebe um request de entrada, formata um evento de log, e chama o método async put na queue. Se definires um tamanho máximo ao criar a queue, tens backpressure automático. Quando a queue está cheia, fazer await do método put pausa o producer até que se liberte espaço. Isto evita que um pico enorme de tráfego esgote a memória do teu sistema. Do outro lado do pipe, tens uma background task separada a atuar como consumer. Esta task corre num loop contínuo. Ela chama o método async get na queue. Se a queue estiver vazia, o consumer adormece em segurança. O event loop acorda-o no momento exato em que um producer coloca um novo evento de log no pipe. O consumer pega no evento, escreve-o no disco, e depois sinaliza que esse job específico está concluído chamando um método chamado task done. Gerir este fluxo durante o teardown da aplicação é crítico. Se precisares de encerrar o teu web server gracefully, queres garantir que todos os eventos de log na queue são realmente escritos no disco. A queue tem um método chamado join. Quando fazes await do join, o teu programa bloqueia até que o número de chamadas ao task done corresponda exatamente ao número de itens originalmente colocados na queue. Isto garante que cada pedaço de trabalho foi totalmente processado. Aqui está o insight principal. O Python 3.13 introduziu um novo método na queue chamado shutdown. Anteriormente, parar um loop producer-consumer de forma limpa exigia passar sentinel values especiais, como injetar um objeto None na queue, só para dizer ao consumer para sair do seu loop. Agora, podes simplesmente chamar o shutdown. Quando fazes isto, qualquer task atualmente bloqueada à espera de fazer put ou get de um item é imediatamente atingida por uma exception QueueShutDown. Apanhas esta exception nas tuas worker tasks, limpas os teus recursos, e sais de forma limpa sem qualquer sentinel logic frágil. Ao desenhar um sistema asyncio, lembra-te que as queues não são apenas data structures; são mecanismos de flow control que lidam nativamente com backpressure, mantendo o teu memory footprint estável mesmo quando os producers superam largamente os consumers. É tudo por este episódio. Obrigado por ouvires, e continua a construir!
12

Networking de Alto Nível com Streams

3m 38s

Mergulhe nas IO Streams de alto nível. Discutimos o StreamReader, o StreamWriter e por que razão omitir o await writer.drain() pode destruir silenciosamente a memória do seu servidor.

Download
Daqui fala o Alex do DEV STORIES DOT EU. asyncio, episódio 12 de 20. Estás a enviar dados por uma ligação de rede e o teu loop parece perfeitamente normal. Mas, nos bastidores, a tua aplicação está a consumir silenciosamente gigabytes de memória até que o sistema a mate. O problema geralmente resume-se a uma linha de código em falta que lida com o flow control. É por isso que hoje vamos olhar para networking de alto nível com streams. O asyncio fornece uma API de alto nível para trabalhar com ligações de rede sem tocar em raw sockets ou protocolos de transporte de baixo nível. Para estabelecer uma ligação TCP, usas uma top-level function chamada open_connection. Passas-lhe uma string para o host e um integer para o port. Devolve imediatamente um tuple de dois objetos: um StreamReader e um StreamWriter. Se estiveres a construir um server em vez de um client, usas o start_server. Forneces uma função de callback, um host e um port. Sempre que um novo client se liga, o asyncio aciona o teu callback, passando-lhe um reader e um writer dedicados para essa ligação específica do client. O StreamReader é a tua interface para receber dados. Fornece métodos assíncronos para puxar bytes da rede. Podes ler um número máximo específico de bytes usando o método read. Se estiveres a fazer parsing de protocolos baseados em linhas, podes ler até um separador específico, como um newline, usando o método readuntil. Se o teu protocolo exigir um header de tamanho fixo, podes usar o readexactly, que vai esperar até que esse número exato de bytes chegue. Como todas estas operações dependem do tráfego de rede e da latência, elas pausam a coroutine, o que significa que tens de fazer await delas. Agora, a segunda parte disto é o StreamWriter. Este objeto lida com o envio de dados para fora. Usas o método write para empurrar bytes para a stream. Aqui está o ponto chave. O método write é uma função regular, não é assíncrona. Não fazes await dele. Quando chamas o write, não estás a colocar dados instantaneamente no cabo de rede. Estás simplesmente a colocar dados num buffer interno do asyncio. O event loop subjacente tenta fazer flush deste buffer para a rede em background. É neste buffer que os developers encontram problemas. Pensa num client TCP a enviar um payload de um ficheiro enorme para um server lento. Se colocares a tua chamada do write num tight loop a ler chunks de um disco local, o Python vai ler o ficheiro incrivelmente mais rápido do que a rede o consegue transmitir. Como o write não bloqueia o teu código, o teu loop continua a girar. O buffer interno absorve o ficheiro inteiro e consome toda a memória disponível do sistema. É aqui que entra a backpressure. Para gerir o flow control, tens de emparelhar as tuas chamadas do write com o método drain. O método drain é assíncrono, o que significa que fazes await dele. Quando fazes await do drain, dizes ao event loop para pausar a tua coroutine se o buffer interno tiver excedido a sua high-water mark. O teu código espera até que o processo em background empurre dados suficientes pela rede para encolher o buffer para um tamanho seguro. A rede ganha tempo para recuperar o atraso, o buffer é limpo e o teu uso de memória permanece estável. Quando terminares de enviar o teu ficheiro, chamas o método close no writer. Tal como o write, o close não é uma função async. Para garantir que a ligação é realmente encerrada de forma limpa e que todos os bytes finais sofrem flush antes de o teu programa avançar, segues isso fazendo await do método wait_closed. O StreamWriter faz com que escrever numa rede pareça instantâneo, mas a física continua a aplicar-se. Faz sempre await do drain depois de fazeres write, para garantir que a tua aplicação respeita a velocidade real da ligação de rede. Obrigado por ouvirem, happy coding a todos!
13

Construir Servidores Async

3m 57s

Construa servidores de rede altamente concorrentes. Aprenda como o asyncio.start_server abstrai as ligações de clientes, criando uma task isolada para cada peer.

Download
Olá, daqui é o Alex da DEV STORIES DOT EU. asyncio, episódio 13 de 20. Criar um servidor TCP altamente concorrente em Python geralmente significa lidar com thread pools ou configurações complexas de event loop. Na verdade, consegues gerir milhares de ligações em menos de dez linhas de código. É exatamente isso que vamos abordar hoje, ao criar servidores assíncronos com streams do asyncio. A base de um servidor de rede em asyncio é uma função chamada start_server. Passas-lhe três coisas: uma função de callback, um endereço IP e uma porta. Quando fazes await do start_server, ele liga-se a esse endereço e começa a escutar ligações TCP de entrada na interface de rede que especificaste. Os developers costumam presumir que precisam de intercetar manualmente estas ligações de entrada e escrever código boilerplate para as despachar para worker threads ou tasks de background personalizadas. Isso é completamente desnecessário. A framework trata da concorrência por ti. Sempre que um novo cliente se liga à tua porta, o start_server cria automaticamente uma nova task do asyncio dedicada inteiramente a esse cliente específico. Imagina criar um servidor de chat simples. Quando o teu primeiro utilizador se liga, o start_server aciona a tua função de callback e entrega-lhe dois objetos: um stream reader e um stream writer. Se mais cinquenta utilizadores se ligarem em simultâneo, cinquenta tasks separadas arrancam instantaneamente para executar exatamente a mesma função de callback. Cada task recebe o seu próprio par isolado de reader e writer. Dentro da tua função de callback, escreves a lógica como se estivesses a falar com apenas uma pessoa de cada vez. Usas o objeto reader para escutar as mensagens recebidas. Fazes await de um método read no reader, especificando um número máximo de bytes que queres aceitar, como cem bytes. O reader dá-te bytes brutos da rede, que tu descodificas para uma string de texto normal. Para responder ao cliente, invertes o processo. Codificas a tua string de resposta de volta para bytes e passas diretamente para o objeto writer. Aqui está o ponto chave. Passar dados para o writer não é uma operação assíncrona, mas garantir que os dados realmente saem da máquina física é. Depois de dares os dados ao writer, tens de fazer await do método drain do writer. O draining pausa a tua task de cliente atual até que o network buffer do sistema operativo tenha espaço livre suficiente para enviar os bytes pela rede. Este passo é crucial porque impede que o teu servidor consuma toda a memória disponível caso um cliente tenha uma ligação de rede lenta. Quando a conversa termina, ou se o cliente se desligar, dizes ao writer para fechar. A seguir, fazes await do método wait_closed do writer para garantir que todos os bytes finais são transmitidos e que o socket subjacente é fechado corretamente. De volta à tua função principal de setup, o start_server devolveu um objeto server. Por defeito, o server para de escutar se o script Python principal chegar ao fim das suas instruções. Para manteres a tua sala de chat aberta indefinidamente, pegas nesse objeto server e fazes await do seu método serve_forever. Isto prende a task principal do asyncio num loop infinito, aceitando silenciosamente novas ligações e criando novas tasks de cliente em background. O verdadeiro poder deste design é que abstrai a complexidade da rede. Escreves código simples e sequencial para uma única ligação isolada, e o event loop escala-o automaticamente por tasks concorrentes. Se quiseres apoiar o programa, podes procurar por DevStoriesEU no Patreon. É tudo por este episódio. Obrigado por ouvires, e continua a programar!
14

Subprocesses Não-bloqueantes

3m 28s

Execute comandos de shell de forma assíncrona. Descubra por que razão usar o módulo subprocess padrão interrompe o event loop, e como o asyncio.create_subprocess_exec resolve isso.

Download
Olá, daqui fala o Alex do DEV STORIES DOT EU. asyncio, episódio 14 de 20. Crias uma web API assíncrona, disparas um comando standard do sistema num endpoint e, de repente, todas as outras tarefas concorrentes paralisam instantaneamente. Nada se move até que esse comando do sistema termine. O culpado é o módulo standard subprocess do Python, e resolver isto requer subprocessos non-blocking. Chamar uma função como o standard subprocess dot run executa um comando do sistema operativo e espera que ele termine. Numa aplicação Python assíncrona, o event loop corre numa única thread. Quando bloqueias essa thread à espera do sistema operativo, o event loop para. Todos os outros requests concorrentes à tua API ficam congelados. Para corrigir isto, o asyncio fornece as suas próprias funções de subprocess concebidas especificamente para o event loop. A ferramenta principal é o asyncio dot create subprocess exec. Aqui está a ideia chave. Esta função não executa o comando diretamente em Python. Ela pede ao sistema operativo para fazer spawn de um child process, mas em vez de bloquear enquanto espera pelo resultado, cede imediatamente o controlo de volta ao event loop. A tua API lida com outros requests enquanto o programa externo corre. Imagina o cenário de uma web API que converte ficheiros de vídeo usando o FFmpeg. Queres disparar a conversão e fazer stream dos output logs de volta para o utilizador em tempo real. Dentro do teu endpoint async, chamas o create subprocess exec. Passas o nome do programa, FFmpeg, seguido pelos seus argumentos. Para capturar os logs, dizes à função para reencaminhar o standard output e o standard error para pipes do asyncio. A função devolve um objeto Process do asyncio. Este objeto representa o comando do sistema operativo a correr e dá-te hooks assíncronos para interagires com ele. Como reencaminhaste os outputs para pipes, o objeto Process expõe-nos como stream readers assíncronos. Lês os logs do FFmpeg iterando sobre a stream de standard error de forma assíncrona, já que o FFmpeg normalmente regista os logs aí. Para cada linha que o processo externo produz, o teu async loop acorda, lê a linha e faz stream dela de volta para o utilizador web. Enquanto espera pela próxima linha, o event loop do Python volta imediatamente a servir outros utilizadores. Obténs streaming de logs em tempo real sem congelar o servidor. Se não precisares de fazer stream do output linha a linha, o objeto Process também fornece um método communicate assíncrono. Fazes await do communicate para enviar dados para o standard input e ler todos os dados do standard output e standard error de uma só vez. Isto mantém o loop livre até que o processo externo termine completamente e devolva os dados. Se lidaste com as streams manualmente como no exemplo do FFmpeg, em vez disso fazes await do método wait no objeto Process para esperar que o processo termine e recolher o seu exit code. O event loop não quer saber se o sistema operativo está a fazer a computação real; se o teu código Python esperar de forma síncrona que o sistema operativo responda, toda a tua aplicação assíncrona fica completamente parada. É tudo por este episódio. Obrigado por ouvires, e continua a construir!
15

Futures: A Ponte de Baixo Nível

3m 45s

Desvende a base das instruções await. Examinamos o asyncio.Future, o seu papel como um resultado eventual e como faz a ponte entre código legado de callbacks e a sintaxe moderna.

Download
Olá, daqui é o Alex do DEV STORIES DOT EU. asyncio, episódio 15 de 20. Escreves código assíncrono limpo e moderno, mas eventualmente, tens de interagir com uma library antiga e teimosa que depende inteiramente de callbacks. Não podes fazer await diretamente a um callback, o que quebra todo o teu fluxo assíncrono. O mecanismo que une estes dois mundos são os Futures: a ponte de baixo nível. Vamos esclarecer uma confusão comum imediatamente. As pessoas frequentemente confundem Tasks e Futures. Uma Task é uma subclass específica de um Future. Uma Task encapsula uma coroutine e agenda-a ativamente no event loop, conduzindo a sua execução passo a passo. Um Future não executa nada. Não tem lógica de execução própria. É simplesmente um container de estado. É um primitivo de baixo nível que representa um resultado eventual de uma operação assíncrona. Ao escrever Python moderno, quase nunca instancias um Future diretamente. O event loop cria-os por trás dos panos. Mas quando precisas de encapsular código legacy baseado em callbacks, constróis-os manualmente. Considera um cenário em que estás a usar uma library de protocolo de rede mais antiga. Esta tem um método request que recebe um endereço de rede, um callback de sucesso e um callback de falha. Queres que a tua função async moderna simplesmente faça await nesse request. Eis como resolves este problema. Dentro da tua função async, obténs o event loop em execução e pedes-lhe para criar um novo objeto Future. Neste exato momento, o Future está num estado pending. Está vazio e à espera. A seguir, escreves uma pequena função de callback de sucesso. Quando acionada, esta função recebe os dados de entrada e chama o método set result no teu Future. Também escreves um callback de erro que chama o método set exception no mesmo Future. Passas ambas as funções para o método request legacy e inicias a chamada de rede. Finalmente, fazes await do Future. Aqui está o ponto chave. Fazer await de um Future pending pausa a coroutine atual. Isto devolve o controlo ao event loop, permitindo que outras tasks sejam executadas. O teu código fica congelado nessa instrução await. Entretanto, o cliente legacy faz o seu input e output de rede em background. Quando os dados chegam, o cliente legacy aciona o teu callback de sucesso. O teu callback chama set result no Future. O Future transita imediatamente do estado pending para o estado finished. O event loop repara nesta mudança de estado. Acorda a coroutine que estava à espera desse Future, desempacota o resultado armazenado, e a tua função async retoma a execução como se tivesse feito await de uma coroutine nativa. Se a chamada de rede falhar, o teu callback de erro define uma exception no Future em vez disso. Quando o event loop acorda a coroutine, levanta exatamente essa exception na linha do await. Um Future tem regras rígidas sobre o estado. Só pode sair do estado pending uma única vez. Se um callback tentar chamar set result num Future que já está finished, o Python levanta um invalid state error. Também podes cancelar um Future manualmente. Se o fizeres, este entra num estado cancelled, e qualquer coroutine que esteja a fazer await do mesmo recebe imediatamente um asyncio Cancelled Error. Os Futures fornecem a cola estrutural necessária entre callbacks event-driven e instruções await com aparência procedural. Entender que cada instrução await pausa a execução até que um Future de baixo nível seja marcado como finished, dá-te clareza total sobre como o Python assíncrono realmente opera sob a superfície. É tudo por este episódio. Obrigado por ouvires, e continua a programar!
16

Transports e Protocols

4m 26s

Vá debaixo do capô para ver como o asyncio comunica com o OS. Entenda a relação 1:1, baseada em callbacks, entre Transports (como os bytes se movem) e Protocols (o que os bytes significam).

Download
Olá, daqui fala o Alex da DEV STORIES DOT EU. asyncio, episódio 16 de 20. Quando usas streams asyncio de alto nível, o teu código fica limpo, sequencial e com awaits seguros. Mas por baixo dessas coroutines amigáveis, existe um motor altamente otimizado e baseado em callbacks, que lida com chamadas complexas do sistema operativo. Para perceberes como a tua aplicação Python realmente comunica com a rede, precisas de olhar para os Transports e Protocols. Estas duas abstrações formam a base da comunicação em rede do asyncio. Trabalham sempre em par. O protocol lida com a lógica da aplicação, decidindo que bytes enviar e como interpretar os dados recebidos. O transport lida com a mecânica. Ele não quer saber o que os teus dados significam ou como estão formatados. A sua única função é descobrir como enviar esses bytes pela rede. Hoje, vamo-nos focar exclusivamente na camada de transport. Pensa no que acontece quando escreves diretamente num socket TCP non-blocking. Tens de perguntar ao sistema operativo se o socket está pronto. Tens de lidar com escritas parciais se o buffer da rede estiver cheio. Tens de controlar que bytes foram realmente enviados e quais precisam de ser tentados novamente mais tarde. Um transport asyncio esconde toda esta complexidade. Ele atua como um wrapper opaco à volta do raw socket e das chamadas subjacentes do sistema operativo. Geralmente, nunca instancias um transport tu próprio. Em vez disso, chamas um método do event loop para criar uma ligação de rede. O event loop configura o socket, cria o transport, liga-o ao teu protocol e devolve-te o par. Aqui está o ponto chave. Assim que a ligação é estabelecida, o transport assume o controlo do buffering de input e output. Quando o teu protocol quer enviar uma mensagem, simplesmente passa um bloco de bytes para o método write do transport. O transport não bloqueia o teu código à espera da rede. Ele coloca imediatamente esses bytes no seu próprio buffer interno. O transport depois trabalha com o event loop em segundo plano, disparando as chamadas de socket non-blocking para o sistema operativo. Se o sistema só conseguir aceitar metade dos bytes neste momento, o transport guarda o resto e tenta novamente na próxima iteração do loop. A tua aplicação nunca precisa de gerir essa fila ao detalhe. O controlo de fluxo já está integrado neste mecanismo. Se escreveres dados mais rápido do que a rede os consegue enviar, o buffer interno do transport começará a encher. Assim que atinge um limite predefinido, o transport aciona um callback específico no teu protocol para pausar a escrita. Quando o buffer finalmente esvazia, dispara outro callback para retomar. Do lado da receção, o transport escuta o event loop. Quando o sistema operativo sinaliza que chegaram bytes, o transport extrai-os do socket e envia-os diretamente para o protocol através de um callback. Tudo neste baixo nível é puramente baseado em callbacks. Não há awaitables aqui. Os transports também fornecem métodos padronizados para gerir o ciclo de vida da ligação. Podes fechar um transport de forma controlada, o que lhe diz para terminar de enviar quaisquer dados em buffer antes de fechar o socket com segurança. Se as coisas correrem mal, podes chamar um método abort para encerrar a ligação imediatamente, descartando o que resta na fila. E se o teu protocol precisar de saber com quem está a comunicar, o transport fornece um método para pedir informações extra, permitindo-te espreitar através da abstração e recuperar o endereço IP do socket subjacente ou os detalhes do peer. A abstração de transport é o que permite que o teu código asyncio permaneça focado exclusivamente na lógica de dados. Os transports isolam a tua aplicação da mecânica caótica do I/O non-blocking; eles recebem raw bytes do teu protocol e lidam silenciosamente com o buffering, as novas tentativas e as chamadas de socket do sistema operativo necessárias para os mover pela rede. É tudo por este episódio. Obrigado por ouvires, e continua a desenvolver!
17

Threading num Mundo Async

3m 17s

Faça a ponte entre os mundos síncrono e assíncrono. Aprenda a descarregar código bloqueante pesado com segurança usando executors e callbacks thread-safe sem bloquear o loop.

Download
Olá, daqui é o Alex da DEV STORIES DOT EU. asyncio, episódio 17 de 20. Colocas uma background thread standard no teu web server async para lidar com uma task lenta, e de repente a tua aplicação começa a fazer deadlock ou a lançar erros de estado enigmáticos. Misturar threads standard com um event loop async é uma receita para o desastre, a não ser que uses as pontes thread-safe designadas. Hoje vamos falar sobre Threading num Mundo Async. A regra principal do asyncio é que o event loop corre numa única thread. Por causa disso, quase todos os objetos asyncio não são thread-safe. Um erro comum é fazer spawn de uma background thread standard, fazer algum trabalho, e depois tentar resolver um future async ou agendar um callback diretamente dessa thread. Se mexeres num objeto asyncio a partir de uma thread diferente daquela que está a correr o event loop, vais corromper o estado do loop. Para enviar uma mensagem de uma background thread para o teu event loop, tens de usar call soon threadsafe. Este é um método do próprio loop. Dás-lhe a função de callback que queres correr e os argumentos. Em vez de o executar imediatamente, a tua background thread coloca esse callback numa queue interna segura. O event loop principal verifica esta queue e executa o teu callback em segurança na main thread durante o seu ciclo normal. Esta é a única maneira segura de uma thread externa interagir com o event loop. Agora considera a situação inversa. Estás a correr o teu event loop async, e precisas de executar um bocado de código síncrono e bloqueante. Um cenário clássico é fazer uma query a um driver PostgreSQL lento e síncrono, como o psycopg2. Se executares uma query à base de dados de cinco segundos diretamente dentro do teu request handler async, todo o teu web server para. O event loop não consegue processar mais nenhum tráfego de rede ou timers até que essa query à base de dados retorne. Aqui está o ponto chave. Para evitar que o loop congele, passas esse trabalho bloqueante para uma thread separada usando run in executor. Este é outro método do event loop. Passas-lhe um thread pool executor e a tua função síncrona de base de dados. O loop entrega a função a uma background thread no pool e retorna imediatamente um objeto awaitable. Fazes await desse objeto. Enquanto a tua query à base de dados corre na background thread, o teu event loop fica totalmente livre para pausar essa task específica e ir tratar de centenas de outros web requests. Assim que o driver de PostgreSQL finalmente retornar os dados, a thread pool passa o resultado de volta para o event loop em segurança. O teu awaitable é resolvido, e a tua função async original retoma a execução exatamente onde parou, agora com os resultados da base de dados. Tens duas pontes unidirecionais. Usa call soon threadsafe para enviar eventos de uma worker thread para o teu loop async. Usa run in executor para enviar trabalho síncrono bloqueante do teu loop async para uma worker thread. Nunca deixes uma chamada síncrona sequestrar o teu event loop, e nunca deixes uma background thread mexer nos teus objetos async diretamente. É tudo por este episódio. Obrigado por ouvires, e continua a desenvolver!
18

Async Generators e Cleanup

3m 36s

Evite fugas de recursos com async generators. Exploramos por que razão a iteração 'async for' pode deixar ligações pendentes quando interrompida, e como o aclosing() oferece segurança.

Download
Olá, daqui é o Alex do DEV STORIES DOT EU. asyncio, episódio 18 de 20. Atinges um timeout ao fazer fetch de rows de uma base de dados. O teu código trata a exception e continua, mas dias depois a tua aplicação vai abaixo porque o teu connection pool está completamente esgotado. Saíste de um loop assíncrono mais cedo, e isso deixou silenciosamente ligações à base de dados abertas em background. A solução está em dominar Async Generators e Cleanup. Quando escreves um async generator para fazer yield de items ao longo do tempo, muitas vezes geres recursos. Considera um cursor de base de dados. Escreves um generator que adquire uma ligação, faz yield das rows uma a uma, e usa um bloco try-finally para devolver essa ligação ao pool quando o fetch termina. Se iterares por cada uma das rows, o generator termina, chega ao bloco finally e faz o cleanup. Tudo funciona. O perigo surge quando não consomes o generator até ao fim. Se a tua iteração estiver dentro de um timeout, ou se simplesmente atingires um break depois de encontrares a row de que precisas, o generator pausa. Fica suspenso no último yield. Não chegou ao bloco finally. A tua ligação à base de dados continua aberta. Podes esperar que o garbage collector do Python acabe por lidar com isto. Em código síncrono, quando um generator perde todas as referências e é apanhado pelo garbage collector, o Python injeta uma exception de saída que corre os blocos finally. Mas o código assíncrono complica isto. O garbage collection é um processo síncrono. Quando o garbage collector finalmente encontra o teu async generator suspenso, não consegue correr código de teardown assíncrono de forma fiável. O event loop pode estar ocupado, ou pode até já estar fechado. Confiar no garbage collector para fazer o cleanup de um async generator resulta em comportamento imprevisível e dangling resources. Esta é a parte que interessa. A documentação oficial do asyncio diz explicitamente que nunca deves confiar no garbage collection para o cleanup de async generators. Tens de os fechar manualmente. A standard library fornece uma ferramenta direta para isto chamada aclosing, que se encontra no módulo contextlib. Atua como um context manager assíncrono. A sua única função é garantir que o método aclose do generator é chamado e que se faz await do mesmo no momento em que terminas de o usar. Em vez de passares o teu generator diretamente para um loop async for, fazes-lhe um wrap. Primeiro, crias a instância do generator. Depois, passas isso para um statement async with aclosing. Dentro desse bloco de contexto, corres o teu loop async for. Quando estruturas o teu código desta forma, um early exit aciona o context manager. Se um timeout interromper o loop, o bloco async with apanha a saída. Ele faz await explicitamente do método aclose no generator. Isto injeta de forma segura a exception de saída no generator suspenso enquanto ainda estás a correr ativamente no event loop. O teu bloco finally é executado imediatamente, fazendo await de quaisquer passos de teardown necessários, e a tua ligação à base de dados volta em segurança para o pool. Sempre que um async generator adquire ligações de rede, file descriptors ou locks de base de dados, faz wrap num aclosing antes de iterar para garantir um cleanup determinístico, independentemente de timeouts ou early breaks. Por este episódio é tudo. Obrigado por ouvires, e continua a construir!
19

Dominar o Debug Mode

3m 43s

Apanhe bugs de concorrência instantaneamente. Aprenda a usar o PYTHONASYNCIODEBUG para fazer profiling de callbacks lentos, descobrir coroutines sem await e identificar exceções nunca recuperadas.

Download
Olá, daqui é o Alex da DEV STORIES DOT EU. asyncio, episódio 19 de 20. O teu servidor de produção está a sofrer picos de lag misteriosos, e as tuas operações em background estão a engolir erros aleatoriamente, sem deixar rasto. O problema não está na lógica da tua aplicação, mas sim na forma como o asyncio standard esconde erros de concurrency para poupar performance. Dominar o debug mode é a solução para expor instantaneamente estas falhas. O debug mode do asyncio funciona como um strict mode para o event loop. Por default, o asyncio prioriza a velocidade bruta em detrimento das verificações de segurança em runtime. Isto significa que, quando as coisas correm mal, muitas vezes falham silenciosamente. Podes ativar o debug mode globalmente definindo a variável de ambiente PYTHONASYNCIODEBUG para um, ou correndo o Python com a flag traço X dev. Também o podes ligar dinamicamente chamando set debug true diretamente no objeto do event loop. Pega no cenário do pico de lag. Tens um web server a lidar com milhares de requests concorrentes e, de repente, um único endpoint faz com que a aplicação inteira congele. Suspeitas que uma operação de regex descontrolada está a bloquear a thread, mas o logging standard apenas te diz quando um request começa ou acaba, não o que bloqueou o loop pelo meio. Quando o debug mode está ativo, o event loop mede o tempo de execução de cada callback. Se um callback bloquear o loop por mais de cem milissegundos, o asyncio faz o log automático de um warning. Este warning inclui o ficheiro e o número da linha exatos onde o freeze ocorreu, apontando-te diretamente para essa pesquisa de regex pesada. Esse limite de cem milissegundos é o default, mas podes ajustá-lo para os teus requisitos específicos de latência, modificando a propriedade slow callback duration no loop. O debug mode também apanha falhas de execução silenciosas. Um erro frequente em código async é chamar uma função coroutine mas esquecer a keyword await. A função devolve um objeto coroutine, mas a lógica em si nunca corre. Na execução normal, esse objeto é descartado silenciosamente. O debug mode faz o tracking disto. Quando o garbage collector limpa uma coroutine unawaited, o debug loop interceta-a e emite um resource warning, mostrando exatamente onde a coroutine órfã foi criada para que possas corrigir a invocação. Esta mesma rede de segurança aplica-se a background tasks. Se uma task do asyncio crashar, a exceção é guardada dentro do próprio objeto da task. Se o teu código nunca fizer um await explícito a essa task ou recuperar o seu resultado, a exceção simplesmente desaparece. Com o debug mode ativado, o asyncio monitoriza o ciclo de vida de cada task. Se uma task for destruída e a sua exceção interna nunca tiver sido recuperada, o event loop faz o log ruidoso do erro juntamente com o traceback, mostrando onde a task foi originalmente criada. Estas verificações adicionam overhead, por isso, normalmente deixas o debug mode desligado em ambientes de produção normais, guardando-o para desenvolvimento local ou troubleshooting direcionado. Aqui está a ideia principal. Ativar o debug mode transfere o peso de encontrar bugs de concurrency silenciosos do teu próprio logging manual de volta para o próprio event loop. Se gostas do podcast e queres apoiar-nos, basta procurares por DevStoriesEU no Patreon. É tudo por este episódio. Obrigado por ouvires, e continua a programar!
20

Estender e Custom Loops

4m 12s

O grande final. Exploramos a integração avançada e o que é necessário para escrever um custom event loop ou criar uma subclasse de BaseEventLoop para ambientes especializados e de alta performance.

Download
Olá, daqui fala o Alex da DEV STORIES DOT EU. asyncio, episódio 20 de 20. Esbarraste num limite de performance com o teu código assíncrono, e o profiling aponta diretamente para o próprio event loop. Não podes reescrever a standard library, mas precisas de controlo de mais baixo nível sobre a forma exata como o sistema lida com sockets e tasks. A resposta está em estender e criar custom loops. O event loop standard do asyncio não é uma caixa negra hardcoded. É uma interface extensível. Foi concebido de raiz para ser totalmente substituível por libraries C de alta performance ou implementações especializadas em Python. A maioria dos developers de aplicações nunca precisará de construir um custom loop. No entanto, se fores autor de uma framework ou se estiveres a construir um loop otimizado como o uvloop, precisas de contornar o comportamento standard e integrar-te diretamente com primitivas de baixo nível do sistema operativo. Para construíres um custom event loop, começas por criar uma subclasse de BaseEventLoop. Esta classe base define todo o contrato sobre como as operações assíncronas se devem comportar. Ao herdares dela, obténs a estrutura, mas podes fazer override de métodos específicos para intercetar e redefinir operações fundamentais. Considera a criação de sockets. Numa aplicação standard, pedes ao asyncio para abrir uma ligação, e ele usa a implementação default de sockets do Python. Mas numa subclasse de um custom loop, podes fazer override dos métodos de criação de rede. Isto significa que, quando a aplicação pede uma ligação de rede, o teu custom loop interceta essa chamada. Podes então encaminhar esse request através de código C altamente otimizado, ou ligá-lo diretamente a funcionalidades avançadas do kernel que o Python standard não expõe. O código da aplicação não muda, mas a infraestrutura subjacente é inteiramente tua. Este controlo granular também se aplica à gestão de tasks. Aqui está o ponto chave. O event loop é responsável por fazer o tracking de cada task assíncrona. Se olhares para o interior do BaseEventLoop, vais encontrar um método interno chamado underscore register task. Ao fazeres override deste método específico, o teu custom loop interceta uma task no exato microssegundo em que ela é criada. Porque é que isto importa? Se estiveres a construir um custom runtime, podes precisar de fazer o tracking de diagnósticos profundos, implementar um memory pool especializado para tasks, ou enviar o estado da task diretamente para um serviço de monitorização customizado. Fazer override do underscore register task dá-te um hook garantido no ciclo de vida de cada coroutine antes mesmo de a sua execução começar. Também podes fazer override do método unregister correspondente para tratar do cleanup exatamente como a tua framework exige. Assim que a tua classe de custom loop estiver construída, tens de dizer ao Python para a usar de facto. Fazes isto criando uma policy de event loop customizada. A policy é apenas uma factory que dita qual a implementação de loop que é criada quando uma thread pede uma. Defines a tua custom policy globalmente. A partir desse ponto para a frente, qualquer função da standard library que peça um event loop receberá a tua versão otimizada e customizada. O verdadeiro poder do asyncio não é apenas a sintaxe async e await. É o facto de que todo o execution engine é uma interface pluggable, pronta para ser trocada no momento em que a performance standard limitar a tua arquitetura. Como isto conclui a nossa série, encorajo-te a ler a documentação oficial, a experimentar estender estes componentes de forma prática, ou a visitar devstories dot eu para sugerir tópicos para futuras séries. É tudo por este episódio. Obrigado por ouvires, e continua a construir!