Torna al catalogo
Season 10 20 Episodi 1h 14m 2026

asyncio

v3.14 — Edizione 2026. Un'analisi approfondita del framework asyncio di Python, che copre l'event loop, le coroutines, la structured concurrency, le primitive di sincronizzazione e i pattern asincroni avanzati. Per Python 3.14.

Python Core Programmazione Asincrona
asyncio
In Riproduzione
Click play to start
0:00
0:00
1
L'Event Loop e il Modello Mentale
Stabilisci il tuo modello mentale di base per asyncio. Scopri come l'event loop agisce come un direttore d'orchestra, gestendo i lavori in modo cooperativo senza fare affidamento sul multithreading.
3m 32s
2
Coroutines contro Awaitables
Demistifichiamo le parole chiave async e await. Esploriamo la distinzione fondamentale tra una funzione coroutine e un oggetto coroutine, e cosa succede effettivamente quando usi await su un'operazione.
3m 35s
3
Il Punto di Ingresso asyncio.run()
Scopri come avviare un'applicazione asyncio in modo sicuro. Discutiamo di asyncio.run, dello spegnimento degli executor e del context manager Runner per i cicli di vita complessi del loop.
3m 41s
4
Pianificazione con i Tasks
Impara a eseguire operazioni in modo concorrente usando asyncio.create_task(). Scopriamo le gravi conseguenze della garbage collection sui task non referenziati.
3m 41s
5
Structured Concurrency con i TaskGroups
Padroneggia la structured concurrency. Comprendi come asyncio.TaskGroup gestisce in modo sicuro molteplici operazioni concorrenti e garantisce chiusure pulite quando si verificano eccezioni.
3m 32s
6
Cancellazione dei Task e Timeout
Esplora i meccanismi di interruzione delle operazioni. Scopri perché viene sollevato asyncio.CancelledError, come gestirlo in un blocco finally e perché non dovresti mai ignorarlo.
4m 05s
7
Cedere il Controllo con Sleep
Comprendi il vero scopo di asyncio.sleep(0). Scopri come cedere il controllo impedisce ai cicli pesanti per la CPU di affamare l'event loop e bloccare l'applicazione.
3m 37s
8
Sincronizzazione: Locks e Mutexes
Previeni le race conditions nel codice async. Esploriamo asyncio.Lock, discutiamo la sua natura non thread-safe e mostriamo perché i lock di threading bloccheranno il tuo event loop.
3m 51s
9
Coordinare lo Stato con gli Events
Impara a trasmettere segnali a più task in attesa. Spieghiamo come asyncio.Event e asyncio.Condition sostituiscono elegantemente i cicli di polling inefficienti.
3m 49s
10
Limitare la Concurrency con i Semaphores
Proteggi le risorse fragili e previeni i ban da rate-limiting. Scopri come asyncio.Semaphore limita l'esecuzione concorrente senza bloccare la tua architettura.
3m 56s
11
Flussi di Lavoro Producer-Consumer
Disaccoppia i producer veloci dai consumer lenti in modo sicuro. Esplora asyncio.Queue, la segnalazione di completamento dei task e le nuove meccaniche di spegnimento per le code.
3m 36s
12
Networking ad Alto Livello con gli Streams
Immergiti negli IO Streams ad alto livello. Discutiamo di StreamReader, StreamWriter e del perché omettere await writer.drain() può distruggere silenziosamente la memoria del tuo server.
3m 54s
13
Costruire Server Async
Costruisci server di rete altamente concorrenti. Scopri come asyncio.start_server astrae le connessioni dei client, generando un task isolato per ogni peer.
3m 58s
14
Subprocesses Non Bloccanti
Esegui comandi shell in modo asincrono. Scopri perché l'uso del modulo subprocess standard ferma l'event loop e come asyncio.create_subprocess_exec risolve il problema.
3m 27s
15
Futures: Il Ponte a Basso Livello
Scopri le fondamenta delle istruzioni await. Esaminiamo asyncio.Future, il suo ruolo come risultato finale e come fa da ponte tra il codice legacy basato su callback e la sintassi moderna.
3m 46s
16
Transports e Protocols
Vai sotto il cofano per vedere come asyncio comunica con il sistema operativo. Comprendi la relazione 1:1 basata su callback tra Transports (come si muovono i byte) e Protocols (cosa significano i byte).
4m 28s
17
Threading in un Mondo Async
Collega i mondi sincrono e asincrono. Impara a scaricare in modo sicuro il codice bloccante pesante usando gli executors e le callback thread-safe senza bloccare il loop.
3m 23s
18
Async Generators e Pulizia
Evita perdite di risorse con gli async generators. Esploriamo perché l'iterazione 'async for' può lasciare connessioni pendenti quando viene interrotta, e come aclosing() garantisce sicurezza.
3m 39s
19
Padroneggiare la Modalità Debug
Cattura i bug di concurrency all'istante. Impara a usare PYTHONASYNCIODEBUG per profilare le callback lente, scoprire le coroutines senza await e individuare le eccezioni mai recuperate.
3m 35s
20
Estendere e Custom Loops
Il finale. Esploriamo l'integrazione avanzata e cosa serve per scrivere un event loop personalizzato o creare una sottoclasse di BaseEventLoop per ambienti specializzati ad alte prestazioni.
3m 49s

Episodi

1

L'Event Loop e il Modello Mentale

3m 32s

Stabilisci il tuo modello mentale di base per asyncio. Scopri come l'event loop agisce come un direttore d'orchestra, gestendo i lavori in modo cooperativo senza fare affidamento sul multithreading.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 1 di 20. Molti sviluppatori, sentendo la parola asincrono, danno per scontato che il loro codice verrà eseguito in parallelo su più core della CPU. Ma poi, esaminando la loro applicazione, scoprono che gira interamente su un singolo thread. Il segreto di questa efficienza senza un vero parallelismo è l'event loop, e comprenderne il modello mentale è il fondamento di asyncio. L'event loop è il gestore centrale dell'esecuzione di qualsiasi applicazione asyncio. È esattamente ciò che suggerisce il nome: un loop continuo che verifica la presenza di operazioni pronte per l'esecuzione, le esegue e poi cerca l'operazione successiva. È fondamentale distinguere questo concetto dal multithreading. In un programma multithread, il sistema operativo controlla l'esecuzione. Il sistema operativo mette forzatamente in pausa un thread e passa a un altro per condividere il tempo della CPU. I thread stessi non hanno alcun controllo su quando vengono messi in pausa. Questo richiede un notevole overhead di sistema per gestire i context switch e proteggere la memoria condivisa. L'event loop opera secondo un modello completamente diverso, chiamato cooperative multitasking. Tutto viene eseguito in sequenza su un singolo thread. Il loop non interrompe mai un'operazione. Si affida invece al codice, che deve cedere esplicitamente il controllo al loop quando deve aspettare qualcosa. Immagina l'event loop come un singolo chef esperto nella cucina affollata di un ristorante. Lo chef riceve più ordini contemporaneamente. Se mette una grande pentola di brodo sul fornello a sobbollire, non rimane lì a fissare il liquido finché non ha finito. Questo approccio bloccherebbe l'intera cucina e non si cucinerebbe nient'altro. Invece, lo chef accende il fornello, lascia sobbollire la pentola e passa immediatamente a tagliare le verdure per un altro piatto. Lo chef rappresenta il singolo thread di esecuzione. L'event loop è lo chef che monitora continuamente la cucina, sapendo esattamente quali pentole stanno sobbollendo, quali padelle devono essere saltate, e passando all'istante al task successivo disponibile. Nel tuo software, una pentola che sobbolle è solitamente un'operazione di input o output. Quando il tuo codice invia una request a un database, il database impiega del tempo per elaborare la query e restituire i dati. Un programma sincrono tradizionale si bloccherebbe in attesa della risposta. Con un event loop, l'operazione registra la sua request e poi comunica al loop di essere in attesa. L'event loop passa immediatamente a un'altra porzione di codice che ha effettivamente dei dati pronti per essere elaborati. Quando il database finalmente risponde, l'operazione originale segnala all'event loop di essere pronta a riprendere. L'event loop la reinserisce nella queue e riprenderà a eseguirla non appena il task corrente cede il controllo. Ecco il punto chiave. Dato che l'event loop non può interrompere forzatamente un'operazione, l'intero sistema si basa del tutto sulla cooperazione. Se un task decide di eseguire un calcolo matematico enorme senza mai cedere il controllo, l'event loop si ferma. Il singolo thread è occupato. Nella nostra cucina, è come se lo chef decidesse di macinare a mano un enorme sacco di farina, ignorando tutti gli altri piatti. Le pentole che sobbollono traboccano, si accumulano nuovi ordini e la cucina si blocca. Il loop è efficiente tanto quanto il codice che ci gira dentro. La vera efficienza asincrona non deriva dall'eseguire più calcoli nello stesso identico momento fisico, ma dall'assicurarsi che il tuo singolo thread non sprechi mai un solo millisecondo in idle mentre aspetta il mondo esterno. Se vuoi aiutarci a portare avanti lo show, puoi sostenerci cercando DevStoriesEU su Patreon. Grazie per l'ascolto e buon coding a tutti!
2

Coroutines contro Awaitables

3m 35s

Demistifichiamo le parole chiave async e await. Esploriamo la distinzione fondamentale tra una funzione coroutine e un oggetto coroutine, e cosa succede effettivamente quando usi await su un'operazione.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 2 di 20. Scrivi una funzione, chiami la funzione, e non succede assolutamente nulla. Il tuo codice gira senza errori, ma il database è vuoto e la network request non parte mai. Il problema è un fraintendimento fondamentale di cosa faccia davvero chiamare una funzione asincrona. Oggi vediamo Coroutine contro Awaitable. Nel Python classico, quando chiami una funzione standard, viene eseguita immediatamente. Le funzioni asincrone infrangono completamente questa regola. C'è una netta differenza tra una coroutine function e un coroutine object. Quando scrivi async def, stai creando una coroutine function. Quando chiami quella funzione nel tuo codice, non esegue il corpo della funzione. Invece, restituisce un coroutine object. Pensala come ordinare un caffè. La funzione async def è la voce sul menu. Chiamare quella funzione è come fare l'ordine alla cassa. Ricevi uno scontrino. Quello scontrino è il tuo coroutine object. Hai dichiarato il tuo intento, ma non hai ancora la tua bevanda, e nessuno ha nemmeno iniziato a prepararla. Per far partire davvero la preparazione e avere il tuo caffè, devi aspettare al bancone. In Python, lo fai usando la keyword await. Quando scrivi await seguito da quel coroutine object, succedono due cose distinte. Primo, la coroutine inizia finalmente a eseguire il suo codice interno. Secondo, la funzione in cui hai messo l'await si mette completamente in pausa. Cede il controllo a Python, dichiarando che non può procedere finché questa specifica coroutine non finisce. Questo comportamento di pausa è la differenza meccanica fondamentale della programmazione asincrona. Mentre la tua funzione è in pausa ad aspettare il caffè, Python è libero di andare a eseguire altro codice altrove. Questo ci porta al termine più ampio di awaitable. Un awaitable è semplicemente qualsiasi oggetto che Python ti permette di usare con la keyword await. Tutte le coroutine sono awaitable. Quando vedi await, leggilo come un comando diretto: esegui questo awaitable object fino al completamento, e sospendi il mio progresso attuale finché non restituisce un risultato finale. Se scrivi una funzione async chiamata fetch data, chiamare semplicemente fetch data restituisce il coroutine object. Se assegni quella chiamata a una variabile chiamata pending request, quella variabile contiene solo la coroutine non eseguita. Il network rimane completamente inattivo. Più avanti nel tuo script, quando scrivi await pending request, Python esegue finalmente la network call. L'esecuzione del tuo blocco di codice attuale si ferma esattamente a quella riga. Una volta che il server risponde, l'espressione await si risolve nei dati restituiti, e il tuo codice circostante continua alla riga successiva. Ecco il punto chiave. Puoi usare la keyword await solo all'interno di una funzione async def. Dato che fare l'await di un oggetto richiede di mettere in pausa l'esecuzione attuale, la funzione che contiene l'await deve a sua volta poter essere messa in pausa. Ecco perché il comportamento asincrono si propaga verso l'esterno. Per fare l'await di una coroutine, devi essere all'interno di una coroutine. Stai costruendo una chain di operazioni sospese, tutte in attesa che il task di livello più basso si risolva. Ricorda, chiamare una funzione async senza farne l'await è solo generare uno scontrino per un lavoro che non hai mai chiesto a nessuno di fare. Il codice non girerà mai finché non ne fai l'await. Grazie per l'ascolto. Alla prossima!
3

Il Punto di Ingresso asyncio.run()

3m 41s

Scopri come avviare un'applicazione asyncio in modo sicuro. Discutiamo di asyncio.run, dello spegnimento degli executor e del context manager Runner per i cicli di vita complessi del loop.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 3 di 20. Un uso improprio del punto di avvio della tua applicazione asincrona può lasciare thread executor orfani e async generator non chiusi. Per evitare resource leak nascosti, devi usare il tool giusto per avviare e fermare la tua applicazione, il che ci porta all'entry point asyncio.run. Molti sviluppatori cercano erroneamente di usare questo tool per eseguire singole coroutine in modo casuale da codice sincrono. Questo non è il suo scopo. Non puoi chiamare la funzione run quando un altro event loop di asyncio è già in esecuzione nello stesso identico thread. Farlo genera immediatamente un runtime error. È progettato specificamente per essere l'unico entry point di alto livello per un programma. Pensa all'inizializzazione del main loop di un web server che coordina tutte le request di traffico in entrata. Hai una funzione asincrona centrale che fa il bind su una porta di rete, imposta i request handler e mantiene il server attivo. Passi questa singola funzione main alla funzione run. Quando fai questo, asyncio gestisce automaticamente l'intero ciclo di vita dell'event loop. Per prima cosa, crea un nuovo event loop e lo imposta come loop attivo corrente per il thread. Successivamente, esegue la coroutine main del tuo web server fino al completamento. Ecco il punto chiave. Il lavoro più importante che fa questa funzione avviene dopo che il tuo codice main ha terminato l'esecuzione. Esegue un cleanup accurato. Prima di restituire il controllo alla parte sincrona del tuo programma, cancella tutti i pending task rimasti. Quindi, arresta in modo sicuro i background thread nel default executor. Infine, finalizza tutti gli async generator prima di chiudere completamente l'event loop. Puoi anche passare un debug flag a questa funzione, che forza il loop sottostante a girare in debug mode per aiutarti a tracciare i problemi di esecuzione. Poiché questa funzione standard fa il teardown di tutto alla fine, crea un confine rigido. Se ti trovi in uno scenario in cui devi eseguire diversi blocchi asincroni distinti da codice sincrono, ma vuoi che condividano lo stesso event loop, chiamare la funzione run standard in sequenza fallirà, perché ogni singola volta viene creato e distrutto un nuovo loop. Per quella situazione, usi il context manager Runner di asyncio. Apri un blocco di contesto usando lo statement with standard di Python. Entrando in questo blocco si inizializza l'event loop. Una volta all'interno, puoi chiamare il metodo run dell'oggetto runner. Gli passi una coroutine, la esegue fino al completamento e restituisce il risultato. Puoi chiamare questo metodo run interno più volte all'interno dello stesso blocco di contesto. L'event loop rimane attivo, mantenendo lo stato, i dati in cache e le connessioni tra quelle chiamate separate. Puoi configurare il context manager quando lo crei passando un debug flag, o persino una custom loop factory se il tuo ambiente richiede un'implementazione specializzata dell'event loop. Quando l'esecuzione finalmente esce dal blocco del context manager, il runner esegue l'esatta stessa sequenza di teardown della funzione standalone. Pulisce gli executor, finalizza i generator e chiude il loop in modo sicuro. La stabilità della tua applicazione dipende interamente da come inizia e finisce. Che tu usi una singola chiamata a funzione o il context manager, instradare la tua esecuzione attraverso questi entry point ufficiali è l'unico modo per garantire che venga fatto un teardown affidabile delle tue risorse asincrone quando il programma esce. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
4

Pianificazione con i Tasks

3m 41s

Impara a eseguire operazioni in modo concorrente usando asyncio.create_task(). Scopriamo le gravi conseguenze della garbage collection sui task non referenziati.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 4 di 20. Avvii un processo in background per inviare le metriche di sistema. Più tardi controlli la tua dashboard e manca metà dei dati. Non è stato lanciato nessun errore. Il tuo codice si è semplicemente fermato in silenzio a metà esecuzione. Questo succede perché hai trattato il tuo background job come fire-and-forget. Oggi parliamo di scheduling con i task, e del perché devi sempre conservare le cose che crei. Quando hai una coroutine che vuoi eseguire in modo concorrente con altro codice, usi la funzione create task di asyncio. Passi la tua coroutine a questa funzione, e asyncio la incapsula in un oggetto Task. Questo dice all'event loop di schedulare il task per l'esecuzione. La funzione ti restituisce immediatamente il nuovo oggetto Task, permettendo al tuo programma principale di continuare a girare mentre il task opera in background. Molti sviluppatori chiamano create task e ignorano il valore di ritorno. Questa è una trappola enorme. Ecco il punto chiave. L'event loop di asyncio mantiene solo delle weak reference ai task che sta eseguendo. Il loop stesso non protegge il tuo task dal garbage collector di Python. Se non assegni l'oggetto Task restituito a una variabile o non lo salvi in una struttura dati, il garbage collector prima o poi si accorgerà che non esistono hard reference. Quando questo succede, Python distrugge l'oggetto task. Non gli importa se la coroutine è nel bel mezzo dell'esecuzione di una query sul database o in attesa di una network response. Il task semplicemente svanisce. Pensa a una funzione async chiamata ship metrics. Formatta un data payload e invia una HTTP request a un server esterno. Chiami create task e gli passi ship metrics, ma non assegni il risultato a niente. Il task inizia a girare. Formatta il payload. Poi arriva alla network call e si mette in pausa in attesa di una connessione. Mentre è in pausa, parte il garbage collector. Il conteggio delle strong reference è zero. Il task viene distrutto. Il server non riceve mai il payload, e la tua applicazione non logga mai un errore perché l'esecuzione ha semplicemente smesso di esistere. Per evitare questo, devi sempre mantenere una strong reference ai task che scheduli. Se stai creando un singolo task, assegnalo a una variabile. Se stai schedulando più background task all'interno di un loop, aggiungili a un set o a una list standard di Python. Finché quel set esiste in memoria, le strong reference esistono, e il garbage collector lascerà in pace i tuoi task in esecuzione. Puoi quindi usare una callback per rimuovere il task dal tuo set una volta che ha finito. La funzione create task accetta anche alcuni argomenti opzionali. Puoi passare una string al parametro name, che assegna un identificatore specifico al task. Questo è caldamente consigliato per il debugging, dato che rende molto più facile rintracciare quale operazione specifica ha fallito se in seguito viene sollevata un'exception. Puoi anche passare un argomento context per stabilire uno specifico stato delle context variable per il task. Trattare le operazioni in background come fire-and-forget alla lunga ti brucerà con dei silent failure. Se chiedi all'event loop di eseguire qualcosa, devi mantenere una hard reference all'oggetto risultante finché il lavoro non è completamente finito. Grazie per aver ascoltato, happy coding a tutti!
5

Structured Concurrency con i TaskGroups

3m 32s

Padroneggia la structured concurrency. Comprendi come asyncio.TaskGroup gestisce in modo sicuro molteplici operazioni concorrenti e garantisce chiusure pulite quando si verificano eccezioni.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 5 di 20. Prima di Python 3.11, lanciare più task concorrenti era facile, ma gestirli in modo sicuro quando uno andava in crash era notoriamente difficile. Spesso ti ritrovavi con task orfani in background che sprecavano risorse silenziosamente. La soluzione a questo casino è la structured concurrency usando TaskGroup. Un TaskGroup è un context manager asincrono. A volte le persone lo confondono con una normale lista di task, ma è molto più rigoroso. Offre forti garanzie di sicurezza su come i task iniziano e finiscono. Impone la regola che una routine padre non può finire finché tutte le sue operazioni figlie non sono completate o cancellate in modo pulito. Lo usi aprendo un blocco async with. All'interno di quel blocco, chiami il metodo create task direttamente sull'oggetto group per avviare le tue operazioni concorrenti. Non butti questi task in un array standard per poi fare await manualmente. Invece, quando il codice raggiunge la fine del blocco async with, il TaskGroup si mette implicitamente in pausa. Aspetta proprio lì finché ogni task generato non finisce. Il blocco semplicemente non uscirà in anticipo. Ecco il punto chiave. La vera potenza di un TaskGroup sta in come gestisce i fallimenti. Con strumenti legacy come gather, se avviavi diversi task e uno lanciava un errore, gli altri continuavano a girare in background. Dovevi scrivere una logica di error handling complessa per rintracciare i sopravvissuti e terminarli. Un TaskGroup gestisce tutto questo in automatico. Prendi lo scenario di un web scraper che fa il fetch di tre endpoint API distinti in simultanea. Ti servono i dati utente, i post recenti e gli alert di sistema. Apri un TaskGroup e generi tre task. Iniziano tutti a girare in modo concorrente sulla rete. A metà dell'operazione, l'endpoint dei post recenti va in timeout e solleva un errore di connessione. Il TaskGroup rileva immediatamente questo fallimento. Intercetta l'errore e invia in automatico un segnale di cancellazione al task dei dati utente e al task degli alert di sistema. Pulisce quelle operazioni in pending in modo che non continuino a mangiare banda di rete o memoria. I task rimanenti sollevano un cancelled error internamente, confermando lo shutdown. Una volta che tutti i task rimanenti sono fermati in sicurezza, il TaskGroup impacchetta l'errore di connessione originale in una nuova struttura chiamata ExceptionGroup, e lo solleva fuori dal blocco di contesto. Questo comportamento rende il tuo codice asincrono completamente prevedibile. Se l'esecuzione supera il blocco con successo, sai per certo che ogni singolo task è andato a buon fine. Se il blocco solleva un ExceptionGroup, sai che il fallimento è stato catturato e che tutto il resto è stato chiuso correttamente. Non lasci mai task impazziti in esecuzione in background. Se ti servono i risultati dei task completati con successo, puoi recuperarli direttamente dagli oggetti task che hai creato, a patto che siano terminati prima che si verificasse il fallimento. Legando i task a un blocco con un ciclo di vita rigoroso, i TaskGroup garantiscono che le operazioni concorrenti entrino ed escano dalla tua applicazione come un'unica unità coordinata. Questo è tutto per oggi. Grazie per aver ascoltato: vai a creare qualcosa di fantastico.
6

Cancellazione dei Task e Timeout

4m 05s

Esplora i meccanismi di interruzione delle operazioni. Scopri perché viene sollevato asyncio.CancelledError, come gestirlo in un blocco finally e perché non dovresti mai ignorarlo.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 6 di 20. Hai scritto un error handler robusto, che cattura tutte le eccezioni generiche nel tuo worker asincrono. Ma ora, durante lo shutdown, il tuo event loop si intasa di task zombie che si rifiutano di morire. La tua rete di sicurezza in realtà li sta intrappolando vivi. Questo è esattamente ciò di cui parliamo oggi: Task Cancellation e Timeout. Quando devi fermare un task in esecuzione, chiami il suo metodo cancel. Questo non termina istantaneamente il task come killare un processo di sistema. Invece, asyncio richiede lo stop iniettando un errore, nello specifico un asyncio CancelledError, nel task. Questo errore viene sollevato esattamente al punto di await corrente o successivo del task. La coroutine quindi fa l'unwind del suo stack proprio come farebbe per qualsiasi errore Python standard. Questo meccanismo è la base anche per i timeout. Quando wrappi un task in una funzione di timeout e il timer scade, l'event loop non ferma magicamente il task. Semplicemente chiama cancel su quel task. Il task riceve il CancelledError al suo prossimo await, fa l'unwind del suo stato e alla fine dice al wrapper del timeout che si è fermato. Solo a quel punto il wrapper del timeout solleva un TimeoutError verso di te. Ecco il punto chiave. Da Python 3.8, CancelledError eredita direttamente da BaseException, non dalla classe Exception standard. Questa scelta di design previene un errore specifico e catastrofico. Gli sviluppatori wrappano abitualmente le operazioni di rete o sui file in blocchi try ed except che catturano le classi Exception generiche per evitare un crash. Se CancelledError fosse una Exception standard, quei blocchi catturerebbero il segnale di cancellazione. Il task magari loggherebbe un warning, ignorerebbe il segnale e continuerebbe l'esecuzione come uno zombie. Spostando CancelledError più in alto nella gerarchia verso BaseException, Python garantisce che i tuoi error handler di tutti i giorni non intercettino accidentalmente una richiesta di cancellazione. Quindi, come gestisci in modo sicuro lo stato quando un task viene cancellato? Ti affidi alla struttura try e finally. Considera un web server che elabora una richiesta HTTP in entrata. L'utente chiede un report enorme, ma poi chiude la finestra del browser. Il server rileva la disconnessione e cancella il task della richiesta. All'interno del tuo codice, stai facendo l'await di una query al database molto lunga. Quell'await solleva improvvisamente un CancelledError. Dato che hai messo la tua interazione col database dentro un blocco try, l'esecuzione salta all'istante al tuo blocco finally. Usi quel blocco finally per fare un rollback pulito della transazione in sospeso e restituire la connessione al database al pool. Una volta che il blocco finally finisce, il CancelledError continua a fare bubble up, terminando con successo il task. A volte un blocco finally non basta. Se devi assolutamente fare del cleanup asincrono, come inviare una richiesta di rete a un microservizio remoto per annunciare l'abort, puoi catturare esplicitamente il CancelledError. Ma se lo fai, devi fare il re-raise esplicito di quell'esatto errore alla fine del tuo blocco except. Non fare il re-raise rompe le meccaniche interne di asyncio. Il task sembrerà aver finito con successo invece di essere stato cancellato, il che corrompe lo stato della tua applicazione e rompe la structured concurrency. La regola da ricordare è che la cancellazione è una richiesta cooperativa, non un comando di kill forzato, e si basa interamente sulle eccezioni che fanno bubble up intatte. Se ti va di supportare lo show, puoi cercare DevStoriesEU su Patreon. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
7

Cedere il Controllo con Sleep

3m 37s

Comprendi il vero scopo di asyncio.sleep(0). Scopri come cedere il controllo impedisce ai cicli pesanti per la CPU di affamare l'event loop e bloccare l'applicazione.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 7 di 20. A volte il segreto per mantenere reattivo il tuo server di rete è dire ai tuoi task più pesanti di fare uno sleep di esattamente zero secondi. Se una funzione non si mette mai in pausa, l'intera applicazione smette di ascoltare il mondo esterno. Per risolvere questo problema, fai yield del controllo usando sleep. Nel framework asyncio, l'event loop esegue esattamente un task alla volta. Si basa interamente sul multitasking cooperativo. Un task viene eseguito continuamente finché non incontra la keyword await, che funge da checkpoint per restituire il controllo dell'esecuzione al loop. Se scrivi una funzione async contenente un'operazione puramente CPU-bound, crei un collo di bottiglia. Pensa al parsing di un enorme payload JSON o alla trasformazione di migliaia di stringhe. Non ci sono punti di await naturali in un normale loop di elaborazione dati. Poiché il task non fa mai yield, l'event loop rimane bloccato. Qualsiasi richiesta di rete in arrivo, risposta del database o health check rimane in coda, in starvation mentre aspetta che il tuo loop finisca. Il modo nativo per risolvere questo problema è restituire manualmente il controllo all'event loop. Lo fai usando un costrutto specifico: fare await di asyncio dot sleep con un argomento pari a zero. A prima vista, fare uno sleep di zero secondi sembra un'operazione inutile. Perché chiedere al sistema di non aspettare affatto? Ecco il punto chiave. Uno sleep di zero secondi non riguarda il trascorrere del tempo. È un segnale esplicito all'event loop. Quando fai await di uno sleep a zero, la coroutine corrente viene immediatamente sospesa. L'event loop prende il controllo, posiziona il tuo task sospeso in fondo alla coda dei task runnable, e verifica se altri task schedulati sono pronti per l'esecuzione. Se un network handler in background è in attesa di confermare una connessione in entrata, arriva il suo turno. Una volta che gli altri task raggiungono i propri punti di await o terminano, il tuo task originale torna in cima alla coda e riprende esattamente da dove si era interrotto. Applichiamo questo concetto a uno scenario concreto. Stai scrivendo una funzione async per elaborare milioni di record da un file JSON. Se esegui un while loop di fila, il tuo server sembrerà morto. Invece, introduci una variabile contatore. All'interno del loop, elabori un record e incrementi il contatore. Poi aggiungi una semplice condizione. Se il contatore indica che sono passate cento iterazioni, fai await di asyncio dot sleep zero. Questo suddivide l'enorme computazione in chunk gestibili. Il loop elabora cento record, si fa da parte per permettere al server di rispondere ai ping o accettare nuovi dati, e poi riprende il parsing dei successivi cento. Il numero di iterazioni tra gli yield è un parametro che devi ottimizzare. Fare yield a ogni singola iterazione aggiunge troppo overhead, perché sospendere e riprendere una coroutine ha un piccolo costo computazionale. Fare yield ogni diecimila iterazioni potrebbe comunque bloccare l'event loop per troppo tempo. Cento è un punto di partenza ragionevole per far respirare il loop. Forzare uno sleep di zero secondi è il modo più semplice per mantenere la tua applicazione cooperativa, assicurando che un singolo loop pesante non mandi mai in starvation il resto del tuo sistema. Grazie per l'ascolto, happy coding a tutti!
8

Sincronizzazione: Locks e Mutexes

3m 51s

Previeni le race conditions nel codice async. Esploriamo asyncio.Lock, discutiamo la sua natura non thread-safe e mostriamo perché i lock di threading bloccheranno il tuo event loop.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 8 di 20. Inserisci un threading lock standard nella tua applicazione async per proteggere una risorsa condivisa, e all'improvviso l'intero event loop si blocca completamente. Il lock ha fatto il suo lavoro, ma ha bloccato tutto il resto nel processo. Per risolvere questo problema senza bloccare il loop, usiamo la sincronizzazione: Lock e Mutex. Un Lock di asyncio, spesso chiamato mutex, garantisce l'accesso esclusivo a una risorsa condivisa tra task async. Prima di tutto, dobbiamo chiarire un equivoco comune. Non puoi usare un thread lock standard del modulo threading di Python all'interno di un'applicazione async. Un threading lock opera a livello di sistema operativo. Se non riesce ad acquisire il lock, mette in pausa l'intero thread. Dato che asyncio esegue più task in modo cooperativo su un singolo thread, bloccare quel thread significa che l'event loop si ferma. Nessuna richiesta di rete parte, nessun timer scatta. Tutto si blocca. Un lock di asyncio risolve questo problema essendo task-safe, non thread-safe. Quando un task di asyncio prova ad acquisire un mutex bloccato, non blocca il thread. Invece, si sospende e cede di nuovo il controllo all'event loop. Questo permette ad altri task non correlati di continuare il loro lavoro mentre il primo task aspetta in coda. Caliamo il tutto in uno scenario concreto. Hai un'applicazione con decine di task async che fanno chiamate API esterne. Il tuo token OAuth scade. Due task diversi si accorgono del token scaduto nell'esatto stesso millisecondo. Senza sincronizzazione, entrambi i task invieranno in modo indipendente una richiesta al server di autenticazione per fare il refresh del token. Questo lavoro ridondante può far scattare i rate limit, o invalidare immediatamente il primo token a causa di rigide policy di rotazione. Per evitare questa race condition, crei un singolo lock di asyncio quando inizializzi la tua applicazione. Questo oggetto lock viene passato o condiviso tra tutti i tuoi task API. Ora, guarda il flusso. Il Task A e il Task B rilevano entrambi il token scaduto. Il Task A raggiunge per primo il blocco di sincronizzazione e fa await sul lock. Lo acquisisce con successo. Il Task B arriva una frazione di secondo dopo e fa await sullo stesso lock. Dato che il Task A lo detiene, il Task B va in sleep, lasciando che l'event loop gestisca altre operazioni. Quando più task aspettano lo stesso lock, asyncio li mette in coda. Una volta che il lock viene rilasciato, l'event loop risveglia il primo task in coda. Il Task A richiede in modo sicuro il nuovo token, aggiorna la variabile condivisa del token, e rilascia il lock. In quel momento, l'event loop risveglia il Task B. Il Task B finalmente acquisisce il lock. Tuttavia, prima di fare una chiamata di rete, il Task B controlla di nuovo il token. Vede che il token è già valido, salta lo step di refresh, rilascia il lock e continua con la sua richiesta API principale. Il modo più sicuro per implementare questa logica è usare un context manager asincrono. Nel tuo codice, scrivi un'istruzione async with seguita dall'oggetto lock. Quando l'esecuzione entra in questo blocco, aspetta l'accesso esclusivo. Quando l'esecuzione esce dal blocco, sia normalmente che a causa di un errore che ha fatto crashare il task, rilascia automaticamente il lock. Non devi chiamare manualmente i metodi acquire o release, il che elimina il rischio di lasciare accidentalmente un lock attivo per sempre. Ecco il punto chiave. Un lock di asyncio non protegge il tuo stato da altri thread del sistema operativo; protegge il tuo stato dai tuoi stessi task concorrenti che si pestano i piedi a vicenda mentre attendono altre operazioni. Grazie per essere stato con noi. Spero tu abbia imparato qualcosa di nuovo.
9

Coordinare lo Stato con gli Events

3m 49s

Impara a trasmettere segnali a più task in attesa. Spieghiamo come asyncio.Event e asyncio.Condition sostituiscono elegantemente i cicli di polling inefficienti.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 9 di 20. Hai cinquanta task in attesa di connettersi a un database. Non vuoi assolutamente che continuino a fare polling in un loop, sprecando cicli di CPU mentre verificano se la connessione è pronta. Hai bisogno di un singolo segnale di broadcast che dica a tutti di iniziare a fare query nello stesso identico momento. Questo è esattamente ciò di cui si occupa il coordinamento dello stato con Event e Condition. Un Event di asyncio gestisce un semplice flag booleano interno. All'inizio è false. Prima di guardare il flusso, chiariamo una confusione comune tra Event e Lock. Un Lock garantisce l'accesso esclusivo a esattamente un task alla volta, tenendo fuori gli altri. Un Event fa l'opposto. Notifica simultaneamente più task in attesa, lasciandoli procedere tutti insieme. Pensa a quello scenario della connessione al database. Il tuo task in background sta lavorando per stabilire la connessione. Nel frattempo, i tuoi cinquanta worker task arrivano a un punto in cui hanno bisogno del database. Ogni worker chiama il metodo wait sul tuo oggetto Event condiviso. Dato che il flag interno è false, tutti e cinquanta i task si sospendono. Restano inattivi. Alla fine, il task in background ha successo e chiama il metodo set sull'Event. Il flag diventa true. All'istante, tutti e cinquanta i worker task sospesi si svegliano e riprendono l'esecuzione. Se più tardi hai bisogno di chiudere la connessione, puoi chiamare il metodo clear sull'Event. Il flag torna a false, e qualsiasi chiamata futura a wait si bloccherà di nuovo. Puoi anche controllare lo stato attuale del flag in qualsiasi momento chiamando il metodo is set, che restituisce true o false senza bloccare il task. Questo copre i semplici segnali di broadcast. A volte un singolo flag booleano non basta. Potresti avere più task che devono aspettare che una risorsa condivisa raggiunga uno specifico stato complesso, e hanno bisogno di accesso esclusivo per controllare o modificare quello stato in sicurezza. È qui che entra in gioco Condition di asyncio. Una Condition è costruita attorno a un Lock sottostante. Per fare qualsiasi cosa con una Condition, un task deve prima acquisirla. Una volta acquisita, il task controlla lo stato condiviso. Se lo stato non è quello di cui il task ha bisogno, il task chiama il metodo wait sulla Condition. Ecco il punto chiave. Chiamare wait su una Condition fa due cose contemporaneamente: rilascia il Lock sottostante, permettendo ad altri task di accedere allo stato, e sospende il task corrente. Mentre quel task è sospeso, un altro task può acquisire la Condition, cambiare lo stato condiviso, e poi chiamare il metodo notify. Il metodo notify prende un argomento che specifica esattamente quanti task in attesa svegliare, di default uno. Puoi anche chiamare notify all per svegliare tutti quanti in una volta sola. Quando un task sospeso si sveglia, non riparte immediatamente. Deve aspettare di riacquisire il Lock sottostante prima che il metodo wait ritorni. Dato che un altro task potrebbe prendere il Lock e cambiare lo stato prima che arrivi il turno del task appena svegliato, la chiamata a wait viene quasi sempre messa dentro un loop while che controlla continuamente lo stato desiderato. Una volta che ha ripreso il Lock e lo stato è corretto, può procedere in sicurezza e alla fine rilasciare la Condition. Quando devi decidere tra i due, ricordati che un Event è un semplice segnale di broadcast che dice ai task che è successa un'azione una tantum, mentre una Condition permette ai task di aspettare in sicurezza un cambio di stato complesso senza fare continuamente polling su una risorsa bloccata. Grazie per aver passato qualche minuto con me. Alla prossima, stammi bene.
10

Limitare la Concurrency con i Semaphores

3m 56s

Proteggi le risorse fragili e previeni i ban da rate-limiting. Scopri come asyncio.Semaphore limita l'esecuzione concorrente senza bloccare la tua architettura.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 10 di 20. Inviare diecimila richieste asincrone a un'API di terze parti fragile è un modo estremamente efficace per farti bannare permanentemente l'indirizzo IP. Il tuo codice funziona perfettamente, ma il server dall'altra parte collassa a causa dell'improvviso picco di traffico. Per proteggere i servizi esterni e il tuo accesso, devi fare throttling della tua applicazione. Questo scudo è la limitazione della concurrency tramite semafori. È utile chiarire subito un equivoco comune. Un semaforo non è un rate limiter. Non limita il numero di richieste che il tuo programma effettua al secondo. Piuttosto, limita le operazioni concorrenti. Controlla rigorosamente quanti task possono eseguire un blocco specifico di operazioni di rete o di file nello stesso identico momento. Se un task termina la sua chiamata API in dieci millisecondi, quello slot si libera immediatamente per il task successivo in coda. Potresti comunque elaborare centinaia di operazioni al secondo, a patto che non ci siano in esecuzione simultaneamente più operazioni del limite consentito. Un semaforo asyncio gestisce un semplice contatore interno. Quando crei l'oggetto semaforo, fornisci un valore iniziale. Prendiamo lo scenario di limitare le richieste HTTP in uscita verso un'API esterna delicata a esattamente dieci connessioni simultanee. Inizializzi il tuo semaforo con un valore di dieci. Prima che qualsiasi task asincrono effettui una richiesta di rete, deve acquisire il semaforo. Questa azione decrementa il contatore interno di uno. Quando la richiesta di rete termina, il task rilascia il semaforo, incrementando nuovamente il contatore di uno. Ecco il punto chiave. Se dieci task hanno già acquisito il semaforo, il contatore si ferma a zero. Quando l'undicesimo task tenta di acquisirlo, quel task viene sospeso. Il metodo acquire blocca l'esecuzione finché uno dei primi dieci task non termina e rilascia la sua presa. Questo semplice lock numerico ti garantisce di non superare mai il tuo hard limit di dieci connessioni attive. Nell'utilizzo pratico, dovresti chiamare raramente i metodi acquire e release manualmente. Invece, usi il semaforo come un context manager asincrono. Incapsulando la tua richiesta HTTP in uno statement async with, Python garantisce che il semaforo venga rilasciato all'uscita del blocco di codice. Questo rilascio avviene anche se l'API va in timeout, fa cadere la connessione o solleva un'eccezione non gestita. Se provi a fare dei release manuali e un errore salta la tua chiamata di release, quello slot di concurrency è perso per sempre. Se perdi tutti e dieci gli slot a causa di errori di rete temporanei, l'intero programma va in deadlock silenziosamente. Esiste un sottile pericolo con il semaforo standard. Se un errore logico nel tuo codice fa sì che un task rilasci il semaforo più volte di quante lo abbia acquisito, il contatore interno aumenterà oltre il tuo limite originale di dieci. Improvvisamente, il tuo scudo di concurrency è rotto e stai inviando involontariamente dodici o quindici richieste simultanee. Per evitare questo, dovresti usare un Bounded Semaphore di asyncio. Un Bounded Semaphore si comporta esattamente come un semaforo standard, ma tiene traccia del valore iniziale che gli hai dato. Se un task fuori controllo tenta di rilasciare il semaforo oltre quel limite iniziale, il Bounded Semaphore solleva immediatamente un ValueError. Va in crash subito e in modo evidente, anziché sovraccaricare silenziosamente l'API esterna. Usa sempre un Bounded Semaphore di default, a meno che tu non abbia una ragione architetturale molto specifica per aumentare dinamicamente i tuoi limiti di concurrency. I Bounded Semaphore intercettano gli errori logici di release nel momento stesso in cui si verificano, mantenendo rigorosi i tuoi limiti di connessione all'API e facendo girare i tuoi sistemi in modo prevedibile. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
11

Flussi di Lavoro Producer-Consumer

3m 36s

Disaccoppia i producer veloci dai consumer lenti in modo sicuro. Esplora asyncio.Queue, la segnalazione di completamento dei task e le nuove meccaniche di spegnimento per le code.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 11 di 20. Hai un web server async che gestisce migliaia di richieste al secondo, e per ogni richiesta devi scrivere un log su disco. Se il tuo server aspetta che la scrittura su disco finisca prima di rispondere, le tue performance crollano. Il modo più affidabile per disaccoppiare producer veloci da consumer lenti in Python async è integrato direttamente nella standard library. Oggi guardiamo i workflow Producer-Consumer usando le code di asyncio. Alcuni sviluppatori che vengono dalla programmazione multi-thread pensano di dover avvolgere questa coda in dei lock per prevenire le race condition. Non serve. La coda di asyncio è progettata specificamente per task concorrenti che girano su un singolo event loop. È intrinsecamente sicura per questi task. Lascia le code thread-safe del modulo queue standard per il threading; usa la versione di asyncio per l'async. Pensa alla coda come a una pipe. Da una parte, hai dei producer che inseriscono elementi. Dall'altra parte, hai dei consumer che li tirano fuori. Usiamo quello scenario di logging. Il tuo web request handler è il producer. Riceve una request in entrata, formatta un evento di log e chiama il metodo asincrono put sulla coda. Se imposti una dimensione massima quando crei la coda, ottieni una backpressure automatica. Quando la coda è piena, fare await sul metodo put mette in pausa il producer finché non si libera spazio. Questo impedisce che un picco di traffico eccessivo esaurisca la memoria del tuo sistema. Dall'altro lato della pipe, hai un background task separato che fa da consumer. Questo task gira in un loop continuo. Chiama il metodo asincrono get sulla coda. Se la coda è vuota, il consumer si mette tranquillamente in sleep. L'event loop lo sveglia nell'istante esatto in cui un producer inserisce un nuovo evento di log nella pipe. Il consumer prende l'evento, lo scrive su disco e poi segnala che lo specifico job è completato chiamando un metodo che si chiama task done. Gestire questo flusso durante il teardown dell'applicazione è fondamentale. Se devi fare lo shutdown del tuo web server in modo graceful, vuoi assicurarti che tutti gli eventi di log in coda vengano effettivamente scritti su disco. La coda ha un metodo chiamato join. Quando fai await su join, il tuo programma si blocca finché il numero di chiamate a task done non corrisponde esattamente al numero di elementi originariamente inseriti nella coda. Questo garantisce che ogni singolo pezzo di lavoro sia stato elaborato completamente. Ecco il punto chiave. Python 3.13 ha introdotto un nuovo metodo per le code chiamato shutdown. In precedenza, per fermare in modo pulito un loop producer-consumer serviva passare degli speciali valori sentinella, come iniettare un oggetto None nella coda, solo per dire al consumer di uscire dal suo loop. Ora, puoi semplicemente chiamare shutdown. Quando lo fai, qualsiasi task attualmente bloccato in attesa di fare put o get di un elemento viene immediatamente colpito da un'eccezione QueueShutDown. Puoi catturare questa eccezione nei tuoi worker task, pulire le tue risorse e uscire in modo pulito senza alcuna fragile logica a sentinella. Quando progetti un sistema asyncio, ricorda che le code non sono solo data structure; sono meccanismi di flow control che gestiscono nativamente la backpressure, mantenendo stabile il tuo memory footprint anche quando i producer superano di gran lunga i consumer. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
12

Networking ad Alto Livello con gli Streams

3m 54s

Immergiti negli IO Streams ad alto livello. Discutiamo di StreamReader, StreamWriter e del perché omettere await writer.drain() può distruggere silenziosamente la memoria del tuo server.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 12 di 20. Stai inviando dati tramite una connessione di rete e il tuo loop sembra funzionare perfettamente. Ma dietro le quinte, la tua applicazione sta consumando silenziosamente gigabyte di memoria finché il sistema non la termina. Il problema di solito si riduce a una riga di codice mancante che gestisce il flow control. Ecco perché oggi ci occuperemo di networking di alto livello con gli stream. asyncio fornisce un'API di alto livello per lavorare con le connessioni di rete senza toccare i raw socket o i protocolli di trasporto di basso livello. Per stabilire una connessione TCP, usi una funzione di alto livello chiamata open_connection. Le passi una string per l'host e un integer per la porta. Restituisce immediatamente una tupla di due oggetti: uno StreamReader e uno StreamWriter. Se stai creando un server invece di un client, usi start_server. Fornisci una funzione di callback, un host e una porta. Ogni volta che un nuovo client si connette, asyncio richiama la tua callback, passandole un reader e un writer dedicati per quella specifica connessione client. Lo StreamReader è la tua interfaccia per ricevere dati. Fornisce metodi asincroni per prelevare byte dalla rete. Puoi leggere un numero massimo specifico di byte usando il metodo read. Se stai facendo il parsing di protocolli line-based, puoi leggere fino a un separatore specifico, come un newline, usando il metodo readuntil. Se il tuo protocollo richiede un header di dimensioni fisse, puoi usare readexactly, che aspetterà fino all'arrivo di quel numero esatto di byte. Poiché tutte queste operazioni dipendono dal traffico di rete e dalla latenza, mettono in pausa la coroutine, il che significa che devi farne l'await. Ora, il secondo elemento di tutto questo è lo StreamWriter. Questo oggetto gestisce l'invio dei dati. Usi il metodo write per inserire i byte nello stream. Ecco il punto chiave. Il metodo write è una funzione normale, non asincrona. Non ne fai l'await. Quando chiami write, non stai mettendo istantaneamente i dati sulla rete. Stai semplicemente mettendo i dati in un buffer interno di asyncio. L'event loop sottostante tenta di fare il flush di questo buffer sulla rete in background. È proprio in questo buffer che gli sviluppatori incontrano problemi. Immagina un client TCP che invia un enorme payload di un file a un server lento. Se metti la tua chiamata a write in un tight loop che legge chunk da un disco locale, Python leggerà il file molto più velocemente di quanto la rete possa trasmetterlo. Poiché write non blocca il tuo codice, il tuo loop continua a girare. Il buffer interno assorbe l'intero file, consumando tutta la memoria di sistema disponibile. È qui che entra in gioco la backpressure. Per gestire il flow control, devi associare le tue chiamate a write con il metodo drain. Il metodo drain è asincrono, il che significa che ne fai l'await. Quando fai l'await di drain, dici all'event loop di mettere in pausa la tua coroutine se il buffer interno ha superato il suo high-water mark. Il tuo codice aspetta che il processo in background invii abbastanza dati sulla rete per ridurre il buffer a una dimensione sicura. La rete ha il tempo di mettersi in pari, il buffer si svuota e l'utilizzo della memoria rimane piatto. Quando hai finito di inviare il tuo file, chiami il metodo close sul writer. Proprio come write, close non è una funzione async. Per garantire che la connessione si chiuda effettivamente in modo pulito e che venga fatto il flush di tutti i byte finali prima che il tuo programma vada avanti, lo fai seguire da un await del metodo wait_closed. Lo StreamWriter fa sembrare istantaneo scrivere su una rete, ma le leggi della fisica si applicano comunque. Fai sempre l'await di drain dopo aver fatto una write per garantire che la tua applicazione rispetti la velocità effettiva della connessione di rete. Grazie per l'ascolto, happy coding a tutti!
13

Costruire Server Async

3m 58s

Costruisci server di rete altamente concorrenti. Scopri come asyncio.start_server astrae le connessioni dei client, generando un task isolato per ogni peer.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 13 di 20. Creare un server TCP altamente concorrente in Python di solito significa dover scontrarsi con i thread pool o con configurazioni complesse dell'event loop. In realtà, puoi gestire migliaia di connessioni in meno di dieci righe di codice. Ed è proprio quello che vedremo oggi, creando server asincroni con gli stream di asyncio. La base di un server di rete in asyncio è una funzione chiamata start_server. Le passi tre cose: una funzione di callback, un indirizzo IP e una porta. Quando fai await su start_server, si associa a quell'indirizzo e inizia ad ascoltare le connessioni TCP in entrata sull'interfaccia di rete che hai specificato. Spesso gli sviluppatori pensano di dover intercettare manualmente queste connessioni in entrata e scrivere codice boilerplate per instradarle a worker thread o a task in background personalizzati. Non è assolutamente necessario. Il framework gestisce la concorrenza per te. Ogni singola volta che un nuovo client si connette alla tua porta, start_server genera automaticamente un nuovo task asyncio dedicato interamente a quel client specifico. Pensa di costruire un semplice server per una chat room. Quando il tuo primo utente si connette, start_server innesca la tua funzione di callback e le passa due oggetti: uno stream reader e uno stream writer. Se altri cinquanta utenti si connettono simultaneamente, cinquanta task separati si avviano all'istante per eseguire la stessa identica funzione di callback. Ogni task riceve la propria coppia isolata di reader e writer. All'interno della tua funzione di callback, scrivi la logica come se stessi parlando con una sola persona alla volta. Usi l'oggetto reader per ascoltare i messaggi in arrivo. Fai await su un metodo read del reader, specificando il numero massimo di byte che vuoi accettare, ad esempio cento byte. Il reader ti restituisce i byte grezzi dalla rete, che tu decodifichi in una stringa di testo standard. Per rispondere al client, inverti il processo. Codifichi la tua stringa di risposta di nuovo in byte e la passi direttamente all'oggetto writer. Ecco il punto chiave. Passare i dati al writer non è un'operazione asincrona, ma assicurarsi che i dati lascino effettivamente la macchina fisica lo è. Dopo aver passato i dati al writer, devi fare await sul metodo drain del writer. Il draining mette in pausa il tuo task client corrente finché il buffer di rete del sistema operativo non ha abbastanza spazio libero per spingere i byte sulla rete. Questo passaggio è fondamentale perché impedisce al tuo server di consumare tutta la memoria disponibile se un client ha una connessione di rete lenta. Quando la conversazione termina, o se il client si disconnette, dici al writer di chiudersi. Fai quindi await sul metodo wait_closed del writer per assicurarti che tutti i byte finali vengano trasmessi e che il socket sottostante si chiuda in modo pulito. Tornando alla tua funzione di setup principale, start_server ha restituito un oggetto server. Di default, il server smette di ascoltare se lo script Python principale raggiunge la fine delle sue istruzioni. Per mantenere la tua chat room aperta all'infinito, prendi quell'oggetto server e fai await sul suo metodo serve_forever. Questo blocca il task principale di asyncio in un loop infinito, accettando silenziosamente nuove connessioni e generando nuovi task client in background. Il vero potere di questo design è che astrae la complessità del networking. Scrivi codice semplice e sequenziale per una singola connessione isolata, e l'event loop lo scala automaticamente su task concorrenti. Se vuoi supportare lo show, puoi cercare DevStoriesEU su Patreon. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
14

Subprocesses Non Bloccanti

3m 27s

Esegui comandi shell in modo asincrono. Scopri perché l'uso del modulo subprocess standard ferma l'event loop e come asyncio.create_subprocess_exec risolve il problema.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 14 di 20. Costruisci un'API web asincrona, lanci un comando di sistema standard all'interno di un endpoint, e all'improvviso ogni altro task concorrente si paralizza all'istante. Non si muove nulla finché quel comando di sistema non termina. Il colpevole è il modulo standard subprocess di Python, e per risolvere il problema servono dei subprocess non bloccanti. Chiamare una funzione come il classico subprocess punto run esegue un comando del sistema operativo e aspetta che finisca. In un'applicazione Python asincrona, l'event loop gira su un singolo thread. Quando blocchi quel thread in attesa del sistema operativo, l'event loop si ferma. Ogni altra request concorrente alla tua API rimane congelata. Per risolvere questo problema, asyncio fornisce le sue funzioni subprocess progettate appositamente per l'event loop. Lo strumento principale è asyncio punto create subprocess exec. Ecco il punto chiave. Questa funzione non esegue il comando direttamente in Python. Chiede al sistema operativo di fare lo spawn di un child process, ma invece di bloccarsi in attesa del risultato, cede immediatamente il controllo all'event loop. La tua API gestisce altre request mentre il programma esterno gira. Prendi lo scenario di un'API web che converte file video usando FFmpeg. Vuoi lanciare la conversione e fare lo stream dei log di output all'utente in tempo reale. All'interno del tuo endpoint async, chiami create subprocess exec. Passi il nome del programma, FFmpeg, seguito dai suoi argomenti. Per catturare i log, dici alla funzione di instradare lo standard output e lo standard error su delle pipe di asyncio. La funzione restituisce un oggetto Process di asyncio. Questo oggetto rappresenta il comando del sistema operativo in esecuzione e ti fornisce degli hook asincroni per interagirci. Dato che hai instradato gli output sulle pipe, l'oggetto Process li espone come stream reader asincroni. Leggi i log di FFmpeg iterando sullo stream dello standard error in modo asincrono, dato che FFmpeg di solito logga lì. Per ogni riga prodotta dal processo esterno, il tuo loop async si sveglia, legge la riga e ne fa lo stream all'utente web. Mentre aspetta la riga successiva, l'event loop di Python torna subito a servire altri utenti. Ottieni lo streaming dei log in tempo reale senza bloccare il server. Se non hai bisogno di fare lo stream dell'output riga per riga, l'oggetto Process fornisce anche un metodo communicate asincrono. Fai l'await di communicate per inviare dati allo standard input e leggere tutti i dati di standard output e standard error in una volta sola. Questo mantiene il loop libero finché il processo esterno non finisce completamente e restituisce i dati. Se hai gestito gli stream manualmente come nell'esempio di FFmpeg, fai invece l'await del metodo wait sull'oggetto Process per aspettare che il processo termini e raccogliere il suo exit code. All'event loop non importa se è il sistema operativo a fare l'effettiva computazione; se il tuo codice Python aspetta in modo sincrono che l'OS risponda, l'intera applicazione asincrona è morta in partenza. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
15

Futures: Il Ponte a Basso Livello

3m 46s

Scopri le fondamenta delle istruzioni await. Esaminiamo asyncio.Future, il suo ruolo come risultato finale e come fa da ponte tra il codice legacy basato su callback e la sintassi moderna.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 15 di 20. Scrivi codice asincrono pulito e moderno, ma prima o poi devi interfacciarti con una vecchia e ostinata libreria che si basa interamente sulle callback. Non puoi fare await direttamente su una callback, il che rompe l'intero flusso asincrono. Il meccanismo che fa da ponte tra questi due mondi sono i Future: il ponte di basso livello. Chiariamo subito un equivoco comune. Spesso si confondono i Task e i Future. Un Task è una sottoclasse specifica di un Future. Un Task incapsula una coroutine e la pianifica attivamente sull'event loop, guidandone l'esecuzione passo dopo passo. Un Future non esegue nulla. Non ha una logica di esecuzione propria. È semplicemente un contenitore di stato. È una primitiva di basso livello che rappresenta il risultato finale di un'operazione asincrona. Quando scrivi codice Python moderno, non istanzi quasi mai un Future direttamente. L'event loop li crea dietro le quinte. Ma quando devi incapsulare del codice legacy basato su callback, li costruisci manualmente. Immagina uno scenario in cui stai usando una vecchia libreria per un protocollo di rete. Ha un metodo request che accetta un indirizzo di rete, una callback di successo e una callback di errore. Vuoi che la tua moderna funzione async chiami semplicemente await su questa request. Ecco come colmare il divario. All'interno della tua funzione async, recuperi l'event loop attualmente in esecuzione e gli chiedi di creare un nuovo oggetto Future. In questo preciso istante, il Future si trova in uno stato pending. È vuoto e in attesa. Successivamente, scrivi una piccola funzione di callback di successo. Quando viene attivata, questa funzione prende i dati in arrivo e chiama il metodo set result sul tuo Future. Scrivi anche una callback di errore che chiama il metodo set exception sullo stesso Future. Passi entrambe queste funzioni al metodo request legacy e avvii la chiamata di rete. Infine, fai await sul Future. Ecco il punto chiave. Fare await su un Future in stato pending mette in pausa la coroutine corrente. Restituisce il controllo all'event loop, permettendo ad altri task di essere eseguiti. Il tuo codice rimane bloccato su quell'istruzione await. Nel frattempo, il client legacy esegue le sue operazioni di input e output di rete in background. Quando i dati arrivano, il client legacy attiva la tua callback di successo. La tua callback chiama set result sul Future. Il Future passa immediatamente dallo stato pending allo stato finished. L'event loop nota questo cambio di stato. Risveglia la coroutine che era in attesa su quel Future, estrae il risultato memorizzato, e la tua funzione async riprende l'esecuzione proprio come se avesse fatto await su una coroutine nativa. Se la chiamata di rete fallisce, la tua callback di errore imposta invece un'eccezione sul Future. Quando l'event loop risveglia la coroutine, solleva esattamente quell'eccezione alla riga dell'await. Un Future ha regole rigide riguardo allo stato. Può uscire dallo stato pending una sola volta. Se una callback tenta di chiamare set result su un Future che è già finished, Python solleva un invalid state error. Puoi anche cancellare un Future manualmente. Se lo fai, entra in uno stato cancelled, e qualsiasi coroutine che fa await su di esso riceve immediatamente un asyncio Cancelled Error. I Future forniscono il collante strutturale necessario tra le callback basate sugli eventi e le istruzioni await dall'aspetto procedurale. Capire che ogni istruzione await in definitiva mette in pausa l'esecuzione finché un Future di basso livello non viene contrassegnato come finished, ti dà totale chiarezza su come opera effettivamente Python asincrono sotto la superficie. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
16

Transports e Protocols

4m 28s

Vai sotto il cofano per vedere come asyncio comunica con il sistema operativo. Comprendi la relazione 1:1 basata su callback tra Transports (come si muovono i byte) e Protocols (cosa significano i byte).

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 16 di 20. Quando usi gli stream di alto livello di asyncio, il tuo codice appare pulito, sequenziale e gestito con await in modo sicuro. Ma sotto queste comode coroutine si cela un motore altamente ottimizzato, callback-driven, che gestisce le caotiche chiamate al sistema operativo. Per capire come la tua applicazione Python comunica effettivamente con una rete, devi esaminare i Transport e i Protocol. Queste due astrazioni costituiscono le fondamenta del networking di asyncio. Lavorano sempre in coppia. Il Protocol gestisce la logica dell'applicazione, decidendo quali byte inviare e come interpretare i dati in arrivo. Il Transport si occupa della parte meccanica. Non gli interessa il significato dei tuoi dati o come sono formattati. Il suo unico compito è capire come inviare quei byte sulla rete. Oggi ci concentreremo proprio sul transport layer. Pensa a cosa succede quando scrivi direttamente su un socket TCP non-blocking. Devi chiedere al sistema operativo se il socket è pronto. Devi gestire le scritture parziali se il buffer di rete è pieno. Devi tenere traccia di quali byte sono stati effettivamente inviati e di quali devono essere ritentati in seguito. Un Transport di asyncio nasconde tutta questa complessità. Agisce come un wrapper opaco attorno al raw socket e alle chiamate al sistema operativo sottostante. In genere, non istanzi mai un Transport tu stesso. Invece, chiami un metodo dell'event loop per creare una connessione di rete. L'event loop configura il socket, crea il Transport, lo collega al tuo Protocol e ti restituisce la coppia. Ecco il punto chiave. Una volta stabilita la connessione, il Transport si fa carico del buffering di input e output. Quando il tuo Protocol vuole inviare un messaggio, passa semplicemente un blocco di byte al metodo write del Transport. Il Transport non blocca il tuo codice in attesa della rete. Inserisce immediatamente quei byte nel suo buffer interno. Il Transport poi lavora con l'event loop in background, lanciando le chiamate non-blocking del socket al sistema operativo. Se il sistema in questo momento può prendere solo metà dei byte, il Transport trattiene il resto e riprova all'iterazione successiva del loop. La tua applicazione non deve mai gestire manualmente quella coda. Il flow control è integrato direttamente in questo meccanismo. Se scrivi dati più velocemente di quanto la rete riesca a inviarli, il buffer interno del Transport inizierà a riempirsi. Una volta raggiunto un limite prestabilito, il Transport attiva un callback specifico sul tuo Protocol per mettere in pausa la scrittura. Quando il buffer finalmente si svuota, lancia un altro callback per riprendere. Sul lato ricevente, il Transport resta in ascolto sull'event loop. Quando il sistema operativo segnala che sono arrivati dei byte in ingresso, il Transport li preleva dal socket e li passa direttamente al Protocol tramite un callback. A questo basso livello tutto è puramente callback-driven. Non ci sono awaitable qui. I Transport forniscono anche metodi standardizzati per gestire il ciclo di vita della connessione. Puoi chiudere un Transport in modo graceful, il che gli dice di finire di inviare eventuali dati nel buffer prima di chiudere il socket in modo sicuro. Se le cose vanno male, puoi chiamare un metodo abort per abbattere immediatamente la connessione, scartando tutto ciò che è rimasto nella coda. E se il tuo Protocol ha bisogno di sapere con chi sta parlando, il Transport fornisce un metodo per richiedere informazioni extra, permettendoti di sbirciare attraverso l'astrazione e recuperare l'indirizzo IP del socket sottostante o i dettagli del peer. L'astrazione del Transport è ciò che permette al tuo codice asyncio di rimanere puramente concentrato sulla logica dei dati. I Transport isolano la tua applicazione dai meccanismi caotici dell'I/O non-blocking; prendono i raw byte dal tuo Protocol e gestiscono silenziosamente il buffering, i retry e le chiamate al socket del sistema operativo necessarie per spostarli attraverso la rete. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
17

Threading in un Mondo Async

3m 23s

Collega i mondi sincrono e asincrono. Impara a scaricare in modo sicuro il codice bloccante pesante usando gli executors e le callback thread-safe senza bloccare il loop.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 17 di 20. Inserisci un background thread standard nel tuo web server async per gestire un task lento, e all'improvviso la tua applicazione va in deadlock o lancia errori di stato criptici. Mescolare thread standard con un event loop async è una ricetta per il disastro, a meno che tu non usi gli appositi bridge thread-safe. Oggi parliamo di Threading in un mondo Async. La regola principale di asyncio è che l'event loop gira in un singolo thread. Per questo motivo, quasi tutti gli oggetti asyncio non sono thread-safe. Un errore comune è creare un background thread standard, fare un po' di lavoro, e poi cercare di risolvere un future async o schedulare una callback direttamente da quel thread. Se tocchi un oggetto asyncio da un thread diverso da quello che fa girare l'event loop, corromperai lo stato del loop. Per inviare un messaggio da un background thread al tuo event loop, devi usare call soon threadsafe. Questo è un metodo del loop stesso. Gli passi la funzione di callback che vuoi eseguire e gli argomenti. Invece di eseguirla immediatamente, il tuo background thread inserisce quella callback in una coda interna sicura. L'event loop principale controlla questa coda ed esegue la tua callback in modo sicuro nel main thread durante il suo ciclo normale. Questo è l'unico modo sicuro per un thread esterno di interagire con l'event loop. Ora considera la situazione inversa. Stai facendo girare il tuo event loop async, e devi eseguire un pezzo di codice sincrono e bloccante. Uno scenario classico è fare una query su un driver PostgreSQL lento e sincrono come psycopg2. Se esegui una query al database di cinque secondi direttamente dentro il tuo request handler async, l'intero web server si blocca. L'event loop non può processare nessun altro traffico di rete o timer finché quella query al database non ritorna. Ecco il punto chiave. Per evitare che il loop si blocchi, sposti quel lavoro bloccante su un thread separato usando run in executor. Questo è un altro metodo dell'event loop. Gli passi un thread pool executor e la tua funzione sincrona del database. Il loop affida la funzione a un background thread nel pool e restituisce immediatamente un oggetto awaitable. Fai await su quell'oggetto. Mentre la tua query al database gira nel background thread, il tuo event loop è totalmente libero di mettere in pausa quel task specifico e andare a gestire centinaia di altre web request. Una volta che il driver PostgreSQL restituisce finalmente i dati, il thread pool passa in modo sicuro il risultato all'event loop. Il tuo awaitable si risolve, e la tua funzione async originale riprende l'esecuzione esattamente da dove si era interrotta, avendo ora a disposizione i risultati del database. Hai a disposizione due bridge unidirezionali. Usa call soon threadsafe per inviare eventi da un worker thread al tuo loop async. Usa run in executor per spostare il lavoro sincrono bloccante fuori dal tuo loop async, verso un worker thread. Non permettere mai a una chiamata sincrona di bloccare il tuo event loop, e non permettere mai a un background thread di toccare direttamente i tuoi oggetti async. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
18

Async Generators e Pulizia

3m 39s

Evita perdite di risorse con gli async generators. Esploriamo perché l'iterazione 'async for' può lasciare connessioni pendenti quando viene interrotta, e come aclosing() garantisce sicurezza.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 18 di 20. Vai in timeout mentre recuperi delle righe da un database. Il tuo codice gestisce l'eccezione e va avanti, ma giorni dopo la tua applicazione va in crash perché il tuo connection pool è completamente esaurito. Sei uscito in anticipo da un loop asincrono, e questo ha lasciato silenziosamente aperte le connessioni al database in background. La soluzione sta nel padroneggiare gli Async Generator e il cleanup. Quando scrivi un async generator per fare yield di elementi nel tempo, spesso ti trovi a gestire delle risorse. Pensa a un cursore del database. Scrivi un generatore che acquisisce una connessione, fa yield delle righe una alla volta, e usa un blocco try-finally per restituire quella connessione al pool quando il recupero è terminato. Se iteri su ogni singola riga, il generatore finisce, entra nel blocco finally e fa cleanup. Tutto funziona. Il pericolo si presenta quando non consumi l'intero generatore. Se la tua iterazione è wrappata in un timeout, o se semplicemente incontri un break dopo aver trovato la riga che ti serve, il generatore si mette in pausa. Rimane sospeso all'ultimo yield. Non ha raggiunto il blocco finally. La tua connessione al database è ancora aperta. Potresti aspettarti che il garbage collector di Python prima o poi gestisca questa situazione. Nel codice sincrono, quando un generatore perde tutte le reference e viene raccolto dal garbage collector, Python inietta un'eccezione di uscita che esegue i blocchi finally. Ma il codice asincrono complica le cose. La garbage collection è un processo sincrono. Quando il garbage collector alla fine trova il tuo async generator sospeso, non può eseguire in modo affidabile il codice di teardown asincrono. L'event loop potrebbe essere occupato, o potrebbe addirittura essere chiuso. Affidarsi al garbage collector per fare cleanup di un async generator porta a comportamenti imprevedibili e risorse appese. Questa è la parte importante. La documentazione ufficiale di asyncio afferma esplicitamente che non dovresti mai fare affidamento sulla garbage collection per il cleanup degli async generator. Devi chiuderli manualmente. La standard library fornisce uno strumento diretto per questo scopo chiamato aclosing, che si trova nel modulo contextlib. Agisce come un context manager asincrono. Il suo unico compito è garantire che il metodo aclose del generatore venga chiamato e atteso con await nel momento in cui hai finito di usarlo. Invece di passare il tuo generatore direttamente a un loop async for, lo wrappi. Prima crei l'istanza del generatore. Poi la passi a uno statement async with aclosing. All'interno di quel blocco di contesto, esegui il tuo loop async for. Quando strutturi il tuo codice in questo modo, un'uscita anticipata attiva il context manager. Se un timeout interrompe il loop, il blocco async with intercetta l'uscita. Fa un await esplicito del metodo aclose sul generatore. Questo inietta in modo sicuro l'eccezione di uscita nel generatore sospeso mentre sei ancora in esecuzione attiva nell'event loop. Il tuo blocco finally viene eseguito immediatamente, facendo await di qualsiasi passaggio di teardown necessario, e la tua connessione al database torna in modo sicuro al pool. Ogni volta che un async generator acquisisce connessioni di rete, file descriptor o lock del database, wrappalo in aclosing prima di iterare per garantire un cleanup deterministico, indipendentemente da timeout o break anticipati. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!
19

Padroneggiare la Modalità Debug

3m 35s

Cattura i bug di concurrency all'istante. Impara a usare PYTHONASYNCIODEBUG per profilare le callback lente, scoprire le coroutines senza await e individuare le eccezioni mai recuperate.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 19 di 20. Il tuo server in produzione sta subendo misteriosi lag spike e le tue operazioni in background si mangiano gli errori in modo casuale senza lasciare traccia. Il problema non è la logica della tua applicazione, ma il modo in cui l'asyncio standard nasconde gli errori di concurrency per ottimizzare le performance. Padroneggiare la debug mode è la soluzione per esporre immediatamente questi fallimenti. La debug mode di asyncio agisce come una strict mode per l'event loop. Di default, asyncio dà priorità alla velocità pura rispetto ai controlli di sicurezza a runtime. Questo significa che quando qualcosa va storto, spesso fallisce silenziosamente. Puoi abilitare la debug mode a livello globale impostando la variabile d'ambiente PYTHONASYNCIODEBUG a uno, oppure eseguendo Python con il flag dash X dev. Puoi anche attivarla dinamicamente chiamando set debug true direttamente sull'oggetto event loop. Prendiamo lo scenario del lag spike. Hai un web server che gestisce migliaia di richieste concorrenti, e improvvisamente un singolo endpoint causa il blocco dell'intera applicazione. Sospetti che un'operazione regex anomala stia bloccando il thread, ma il logging standard ti dice solo quando una richiesta inizia o finisce, non cosa ha bloccato il loop nel frattempo. Quando la debug mode è attiva, l'event loop misura il tempo di esecuzione di ogni singola callback. Se una callback blocca il loop per più di cento millisecondi, asyncio genera automaticamente un warning. Questo warning include il file e il numero di riga esatti in cui si è verificato il blocco, puntandoti dritto a quella pesante ricerca regex. Quella soglia di cento millisecondi è il default, ma puoi regolarla in base ai tuoi specifici requisiti di latenza modificando la proprietà slow callback duration sul loop. La debug mode rileva anche i fallimenti di esecuzione silenziosi. Un errore frequente nel codice async è chiamare una funzione coroutine dimenticando la keyword await. La funzione restituisce un oggetto coroutine, ma la logica effettiva non viene mai eseguita. Nella normale esecuzione, quell'oggetto viene scartato silenziosamente. La debug mode tiene traccia di questo. Quando il garbage collector pulisce una coroutine unawaited, il debug loop la intercetta ed emette un resource warning, mostrando esattamente dove è stata creata la coroutine orfana in modo da poterne correggere l'invocazione. Questa stessa rete di sicurezza si applica ai task in background. Se un task asyncio crasha, l'eccezione viene memorizzata all'interno dell'oggetto task stesso. Se il tuo codice non fa mai esplicitamente await su quel task o non ne recupera il risultato, l'eccezione semplicemente svanisce. Con la debug mode abilitata, asyncio monitora il lifecycle di ogni task. Se un task viene distrutto e la sua eccezione interna non è mai stata recuperata, l'event loop logga in modo evidente l'errore insieme al traceback che mostra dove il task era stato originariamente spawnato. Questi controlli aggiungono overhead, quindi in genere lasci la debug mode spenta nei normali ambienti di produzione, riservandola per lo sviluppo locale o per un troubleshooting mirato. Ecco il punto chiave. Abilitare la debug mode sposta l'onere di trovare i concurrency bug silenziosi dal tuo logging manuale direttamente sull'event loop stesso. Se apprezzi il podcast e vuoi supportarci, cerca semplicemente DevStoriesEU su Patreon. Per questo episodio è tutto. Grazie per l'ascolto, e continua a sviluppare!
20

Estendere e Custom Loops

3m 49s

Il finale. Esploriamo l'integrazione avanzata e cosa serve per scrivere un event loop personalizzato o creare una sottoclasse di BaseEventLoop per ambienti specializzati ad alte prestazioni.

Download
Ciao, sono Alex di DEV STORIES DOT EU. asyncio, episodio 20 di 20. Ti sei scontrato con un muro di prestazioni con il tuo codice asincrono, e il profiling punta direttamente al core event loop stesso. Non puoi riscrivere la standard library, ma hai bisogno di un controllo di più basso livello su come esattamente il sistema gestisce socket e task. La risposta sta nell'estendere e creare custom loop. L'event loop standard di asyncio non è una black box hardcoded. È un'interfaccia estensibile. È stato progettato fin dalle fondamenta per essere completamente sostituibile da librerie C ad alte prestazioni o da implementazioni Python specializzate. La maggior parte degli sviluppatori di applicazioni non avrà mai bisogno di creare un custom loop. Tuttavia, se sei l'autore di un framework o stai creando un loop ottimizzato come uvloop, devi bypassare il comportamento standard e integrarti direttamente con le primitive di più basso livello del sistema operativo. Per creare un custom event loop, inizi creando una subclass di BaseEventLoop. Questa base class definisce l'intero contratto su come devono comportarsi le operazioni asincrone. Ereditando da essa, ottieni la struttura, ma puoi fare l'override di metodi specifici per intercettare e ridefinire le operazioni fondamentali. Prendi ad esempio la creazione dei socket. In un'applicazione standard, chiedi ad asyncio di aprire una connessione, e lui usa l'implementazione di default dei socket di Python. Ma in una subclass di un custom loop, puoi fare l'override dei metodi di creazione della rete. Questo significa che quando l'applicazione richiede una connessione di rete, il tuo custom loop intercetta quella chiamata. Puoi quindi instradare quella richiesta attraverso codice C altamente ottimizzato, o collegarla direttamente a funzionalità avanzate del kernel che il Python standard non espone. Il codice dell'applicazione non cambia, ma il meccanismo sottostante è interamente tuo. Questo controllo granulare si applica anche alla gestione dei task. Ecco il punto chiave. L'event loop è responsabile del tracciamento di ogni singolo task asincrono. Se guardi sotto il cofano di BaseEventLoop, troverai un metodo interno chiamato underscore register task. Facendo l'override di questo metodo specifico, il tuo custom loop intercetta un task nell'esatto microsecondo in cui viene creato. Perché è importante? Se stai costruendo un custom runtime, potresti aver bisogno di tracciare diagnostica approfondita, implementare un memory pooling specializzato per i task, o inviare lo stato dei task direttamente a un servizio di monitoraggio custom. Fare l'override di underscore register task ti garantisce un hook nel ciclo di vita di ogni coroutine prima ancora che inizi l'esecuzione. Puoi anche fare l'override del corrispondente metodo unregister per gestire il cleanup esattamente come richiesto dal tuo framework. Una volta costruita la tua classe custom loop, devi dire a Python di usarla effettivamente. Lo fai creando una custom event loop policy. La policy è semplicemente una factory che detta quale implementazione del loop viene creata quando un thread ne richiede una. Imposti la tua custom policy a livello globale. Da quel momento in poi, a qualsiasi funzione della standard library che richieda un event loop verrà consegnata la tua versione custom e ottimizzata. La vera potenza di asyncio non è solo la sintassi async e await. È il fatto che l'intero motore di esecuzione è un'interfaccia pluggable, pronta per essere sostituita nel momento in cui le prestazioni standard limitano la tua architettura. Dato che questo conclude la nostra serie, ti incoraggio a leggere la documentazione ufficiale, a provare a estendere questi componenti hands-on, o a visitare devstories dot eu per suggerire argomenti per le serie future. Questo è tutto per questo episodio. Grazie per l'ascolto, e continua a sviluppare!