Înapoi la catalog
Season 10 20 Episoade 1h 17m 2026

asyncio

v3.14 — Ediția 2026. O analiză aprofundată a framework-ului asyncio din Python, acoperind event loop, coroutines, structured concurrency, synchronization primitives și tipare asincrone avansate. Pentru Python 3.14.

Python Core Programare asincronă
asyncio
Se redă acum
Click play to start
0:00
0:00
1
Event Loop și modelul mental
Stabilește-ți modelul mental de bază pentru asyncio. Află cum un event loop acționează ca un dirijor de orchestră, gestionând sarcinile în mod cooperativ, fără a se baza pe multithreading.
3m 58s
2
Coroutines vs Awaitables
Demistificăm cuvintele cheie async și await. Explorăm distincția critică dintre o funcție coroutine și un obiect coroutine, și ce se întâmplă de fapt atunci când folosești await pe o operațiune.
3m 28s
3
Punctul de intrare asyncio.run()
Descoperă cum să inițializezi o aplicație asyncio în siguranță. Discutăm despre asyncio.run, închiderea executor-ilor și context manager-ul Runner pentru cicluri de viață complexe ale loop-ului.
4m 08s
4
Planificarea cu Tasks
Învață cum să execuți operațiuni concurent folosind asyncio.create_task(). Descoperim consecințele severe ale garbage collection-ului asupra task-urilor fără referință.
3m 37s
5
Structured Concurrency cu TaskGroups
Stăpânește structured concurrency. Înțelege cum asyncio.TaskGroup gestionează în siguranță multiple operațiuni concurente și asigură o oprire curată atunci când apar excepții.
3m 25s
6
Anularea Task-urilor și Timeouts
Explorează mecanismele de abandonare a operațiunilor. Află de ce este declanșată asyncio.CancelledError, cum să o gestionezi într-un bloc finally și de ce nu ar trebui să o ignori niciodată.
4m 13s
7
Cedarea controlului cu Sleep
Înțelege adevăratul scop al asyncio.sleep(0). Descoperă cum cedarea controlului previne ca buclele cu consum mare de CPU să blocheze event loop-ul și să înghețe aplicația.
4m 06s
8
Sincronizare: Locks și Mutexes
Previne race conditions în codul async. Explorăm asyncio.Lock, discutăm natura sa non-thread-safe și arătăm de ce lock-urile de threading îți vor îngheța event loop-ul.
4m 32s
9
Coordonarea stării cu Events
Învață să transmiți semnale către mai multe task-uri aflate în așteptare. Explicăm cum asyncio.Event și asyncio.Condition înlocuiesc elegant buclele ineficiente de interogare.
3m 38s
10
Limitarea concurenței cu Semaphores
Protejează resursele fragile și previne banările din cauza rate-limiting-ului. Descoperă cum asyncio.Semaphore limitează execuția concurentă fără a-ți bloca arhitectura.
3m 59s
11
Fluxuri de lucru Producer-Consumer
Decuplează în siguranță producer-ii rapizi de consumer-ii lenți. Explorează asyncio.Queue, semnalizarea finalizării task-urilor și noile mecanisme de oprire pentru queues.
3m 40s
12
Rețelistică High-Level cu Streams
Aprofundează IO Streams de nivel înalt. Discutăm despre StreamReader, StreamWriter și de ce omiterea await writer.drain() poate distruge în tăcere memoria serverului tău.
4m 02s
13
Construirea serverelor Async
Construiește servere de rețea extrem de concurente. Învață cum asyncio.start_server abstractizează conexiunile clienților, generând un task izolat pentru fiecare peer.
4m 01s
14
Subprocesses Non-blocking
Rulează comenzi shell în mod asincron. Descoperă de ce folosirea modulului standard subprocess oprește event loop-ul și cum asyncio.create_subprocess_exec rezolvă acest lucru.
3m 36s
15
Futures: Puntea Low-Level
Descoperă baza instrucțiunilor await. Examinăm asyncio.Future, rolul său ca rezultat final și modul în care face legătura între codul legacy bazat pe callback-uri și sintaxa modernă.
3m 56s
16
Transports și Protocols
Privește sub capotă pentru a vedea cum asyncio comunică cu sistemul de operare. Înțelege relația 1:1, bazată pe callback-uri, dintre Transports (cum se mișcă octeții) și Protocols (ce înseamnă octeții).
4m 23s
17
Threading într-o lume Async
Creează o punte între lumile sincronă și asincronă. Învață cum să externalizezi în siguranță codul blocant greoi folosind executors și callback-uri thread-safe, fără a bloca loop-ul.
3m 26s
18
Async Generators și Curățarea
Evită scurgerile de resurse cu async generators. Explorăm de ce iterația 'async for' poate lăsa conexiuni suspendate atunci când este întreruptă și cum aclosing() oferă siguranță.
3m 48s
19
Stăpânirea modului Debug
Prinde instantaneu bug-urile de concurență. Învață cum să folosești PYTHONASYNCIODEBUG pentru a profila callback-urile lente, a descoperi coroutines fără await și a identifica excepțiile care nu au fost preluate niciodată.
3m 57s
20
Extinderea și Custom Loops
Finalul. Explorăm integrarea avansată și ce presupune scrierea unui event loop personalizat sau crearea unei subclase BaseEventLoop pentru medii specializate, de înaltă performanță.
3m 45s

Episoade

1

Event Loop și modelul mental

3m 58s

Stabilește-ți modelul mental de bază pentru asyncio. Află cum un event loop acționează ca un dirijor de orchestră, gestionând sarcinile în mod cooperativ, fără a se baza pe multithreading.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 1 din 20. Mulți developeri aud cuvântul asincron și presupun că întregul lor cod se va executa în paralel pe mai multe core-uri CPU. Dar apoi își inspectează aplicația și descoperă că rulează în întregime pe un singur thread. Secretul acestei eficiențe fără paralelism real este event loop-ul, iar înțelegerea modelului său mental este fundamentul asyncio. Event loop-ul este managerul central de execuție al oricărei aplicații asyncio. Este exact ceea ce sugerează și numele: un loop continuu care verifică operațiunile gata de rulare, le execută, și apoi caută următoarea operațiune. Este vital să separăm acest concept de multithreading. Într-un program multithreaded, sistemul de operare controlează execuția. Sistemul de operare va pune forțat pe pauză un thread și va trece la altul pentru a împărți timpul de CPU. Thread-urile în sine nu au niciun control asupra momentului în care sunt puse pe pauză. Acest lucru necesită un overhead de sistem semnificativ pentru a gestiona context switch-urile și a proteja shared memory-ul. Event loop-ul funcționează pe un model complet diferit, numit cooperative multitasking. Totul rulează secvențial pe un singur thread. Loop-ul nu întrerupe niciodată o operațiune. În schimb, se bazează pe cod să cedeze explicit controlul înapoi loop-ului atunci când are de așteptat după ceva. Gândește-te la event loop ca la un singur bucătar expert în bucătăria unui restaurant aglomerat. Bucătarul primește mai multe comenzi simultan. Dacă pune o oală mare de supă pe foc să fiarbă, nu stă în fața aragazului holbându-se la lichid până e gata. Această abordare ar bloca întreaga bucătărie și nimic altceva nu s-ar mai găti. În schimb, bucătarul dă drumul la foc, lasă oala să fiarbă și trece imediat la tăiat legume pentru un alt fel de mâncare. Bucătarul reprezintă singurul thread de execuție. Event loop-ul este bucătarul care scanează continuu bucătăria, știind exact ce oale fierb, ce tigăi trebuie întoarse, și trecând instantaneu la următorul job disponibil. În software-ul tău, o oală care fierbe este de obicei o operațiune de input sau output. Când codul tău trimite un request către o bază de date, baza de date are nevoie de timp pentru a procesa query-ul și a trimite datele înapoi. Un program sincron tradițional s-ar bloca și ar aștepta răspunsul. Cu un event loop, operațiunea își înregistrează request-ul și apoi îi spune loop-ului că așteaptă. Event loop-ul face imediat switch la o altă bucată de cod care are deja date gata de procesat. Când baza de date răspunde în sfârșit, operațiunea inițială semnalează event loop-ului că este gata să fie reluată. Event loop-ul o pune înapoi în queue și îi va relua execuția de îndată ce jobul curent face yield. Iată ideea cheie. Pentru că event loop-ul nu poate opri forțat o operațiune, întregul sistem se bazează în totalitate pe cooperare. Dacă un job decide să facă un calcul matematic masiv fără să dea vreodată yield la control, event loop-ul se oprește. Singurul thread este ocupat. În bucătăria noastră, asta ar fi ca și cum bucătarul ar decide să macine manual un sac imens de făină, ignorând orice alt fel de mâncare. Oalele de pe foc dau în clocot, noile comenzi se adună, iar bucătăria se blochează complet. Loop-ul este doar la fel de eficient pe cât este codul care rulează în interiorul lui. Adevărata eficiență asincronă nu vine din executarea mai multor calcule în exact același moment fizic, ci din a te asigura că singurul tău thread nu irosește nicio milisecundă stând degeaba în timp ce așteaptă după lumea exterioară. Dacă vrei să ne ajuți să continuăm emisiunea, ne poți susține căutând DevStoriesEU pe Patreon. Mulțumim că ne asculți, happy coding tuturor!
2

Coroutines vs Awaitables

3m 28s

Demistificăm cuvintele cheie async și await. Explorăm distincția critică dintre o funcție coroutine și un obiect coroutine, și ce se întâmplă de fapt atunci când folosești await pe o operațiune.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 2 din 20. Scrii o funcție, apelezi funcția și absolut nimic nu se întâmplă. Codul tău rulează fără erori, dar baza de date este goală și network request-ul nu se declanșează niciodată. Problema este o neînțelegere fundamentală a ceea ce face de fapt apelarea unei funcții async. Astăzi, vorbim despre coroutines vs awaitables. În Python-ul standard, când apelezi o funcție normală, aceasta rulează imediat. Funcțiile async încalcă complet această regulă. Există o diferență strictă între o funcție coroutine și un obiect coroutine. Când scrii async def, creezi o funcție coroutine. Când apelezi acea funcție în codul tău, nu se execută corpul funcției. În schimb, returnează un obiect coroutine. Gândește-te la asta ca atunci când comanzi o cafea. Funcția async def este produsul din meniu. Apelarea acelei funcții este ca și cum ai plasa comanda la casă. Primești un bon. Acel bon este obiectul tău coroutine. Ți-ai exprimat intenția, dar încă nu ai băutura și nimeni nici măcar nu a început să o prepare. Pentru a declanșa de fapt procesul de preparare și a-ți primi cafeaua, trebuie să aștepți la tejghea. În Python, faci acest lucru folosind keyword-ul await. Când tastezi await urmat de acel obiect coroutine, se întâmplă două lucruri distincte. În primul rând, coroutine-ul începe în sfârșit să își execute codul intern. În al doilea rând, funcția în care ai plasat await se pune complet pe pauză. Cedează controlul înapoi către Python, declarând că nu poate continua până când acest coroutine specific nu se termină. Acest comportament de pauză este diferența mecanică principală a programării async. În timp ce funcția ta este pe pauză așteptând cafeaua, Python este liber să ruleze alt cod în altă parte. Acest lucru ne aduce la termenul mai larg de awaitable. Un awaitable este pur și simplu orice obiect pe care Python îți permite să îl folosești cu keyword-ul await. Toate coroutine-urile sunt awaitables. Când vezi await, citește-l ca pe o comandă directă: rulează acest obiect awaitable până la capăt și suspendă progresul meu curent până când returnează un rezultat final. Dacă scrii o funcție async numită fetch data, simpla apelare a lui fetch data returnează obiectul coroutine. Dacă atribui acel apel unei variabile numite pending request, acea variabilă conține doar coroutine-ul neexecutat. Rețeaua rămâne complet liniștită. Mai târziu în script-ul tău, când scrii await pending request, Python execută în sfârșit network call-ul. Execuția blocului tău curent de cod se oprește exact la acea linie. Odată ce serverul răspunde, expresia await face resolve în datele returnate, iar codul tău continuă la linia următoare. Iată ideea cheie. Poți folosi keyword-ul await doar în interiorul unei funcții async def. Deoarece a da await unui obiect necesită punerea pe pauză a execuției curente, funcția care conține await trebuie să poată fi ea însăși pusă pe pauză. De aceea, comportamentul async se propagă în exterior. Pentru a da await unui coroutine, trebuie să fii în interiorul unui coroutine. Construiești un chain de operațiuni suspendate, toate așteptând ca task-ul de cel mai jos nivel să facă resolve. Ține minte, apelarea unei funcții async fără să îi dai await înseamnă doar generarea unui bon pentru o muncă pe care nu ai cerut nimănui să o facă. Codul nu va rula niciodată până când nu îi dai await. Mulțumesc că m-ai ascultat. Pe data viitoare!
3

Punctul de intrare asyncio.run()

4m 08s

Descoperă cum să inițializezi o aplicație asyncio în siguranță. Discutăm despre asyncio.run, închiderea executor-ilor și context manager-ul Runner pentru cicluri de viață complexe ale loop-ului.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 3 din 20. Utilizarea greșită a punctului de pornire al aplicației tale asincrone poate lăsa în urmă thread executors suspendați și async generators neînchise. Pentru a evita resource leaks ascunse, trebuie să folosești tool-ul potrivit pentru a porni și opri aplicația, ceea ce ne aduce la entry point-ul asyncio.run. Mulți developeri încearcă în mod eronat să folosească acest tool pentru a executa aleatoriu coroutines individuale din cod sincron. Ăsta nu este scopul lui. Nu poți apela funcția run atunci când un alt event loop asyncio rulează deja în exact același thread. Dacă faci asta, declanșezi imediat o runtime error. Este conceput special pentru a fi singurul entry point high-level pentru un program. Gândește-te la inițializarea unui main loop de web server care coordonează toate request-urile de trafic primite. Ai o funcție asincronă centrală care face bind la un port de rețea, configurează request handlers și menține serverul activ. Pasezi acea singură funcție main în funcția run. Când faci asta, asyncio gestionează automat întregul lifecycle al event loop-ului. Mai întâi, creează un nou event loop și îl setează ca loop activ curent pentru thread. Apoi, execută coroutine-ul tău main de web server până la finalizare. Aici e partea importantă. Cea mai valoroasă muncă pe care o face această funcție are loc după ce codul tău main termină de executat. Face un cleanup riguros. Înainte de a returna controlul părții sincrone a programului tău, anulează orice pending tasks rămase. Apoi, oprește în siguranță background threads din default executor. În cele din urmă, finalizează toate async generators înainte de a închide complet event loop-ul. De asemenea, poți pasa un debug flag acestei funcții, care forțează loop-ul subiacent să ruleze în debug mode pentru a ajuta la urmărirea problemelor de execuție. Pentru că această funcție standard face teardown complet la sfârșit, creează o graniță rigidă. Dacă ai un scenariu în care trebuie să rulezi mai multe blocuri asincrone distincte din cod sincron, dar vrei ca ele să împartă același event loop, apelarea funcției standard run una după alta va eșua, deoarece un nou loop este creat și distrus de fiecare dată. Pentru situația asta, folosești context manager-ul asyncio Runner. Deschizi un context block folosind instrucțiunea standard Python with. Intrarea în acest block inițializează event loop-ul. Odată ajuns înăuntru, poți apela metoda run a obiectului runner. Îi pasezi un coroutine, el îl rulează până la finalizare și returnează rezultatul. Poți apela această metodă run internă de mai multe ori în cadrul aceluiași context block. Event loop-ul rămâne activ, menținând state-ul, datele din cache și conexiunile dintre aceste apeluri separate. Poți configura context manager-ul atunci când îl creezi pasând un debug flag, sau chiar un custom loop factory dacă mediul tău necesită o implementare specializată de event loop. Când execuția iese în sfârșit din block-ul context manager-ului, runner-ul execută exact aceeași secvență de teardown ca și funcția standalone. Curăță executors, finalizează generators și închide în siguranță loop-ul. Stabilitatea aplicației tale depinde în întregime de modul în care începe și se termină. Indiferent dacă folosești un singur apel de funcție sau context manager-ul, rutarea execuției prin aceste entry points oficiale este singura modalitate de a garanta că resurselor tale asincrone li se face teardown în mod fiabil atunci când programul se termină. Asta e tot pentru acest episod. Mersi că m-ai ascultat și spor la construit!
4

Planificarea cu Tasks

3m 37s

Învață cum să execuți operațiuni concurent folosind asyncio.create_task(). Descoperim consecințele severe ale garbage collection-ului asupra task-urilor fără referință.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 4 din 20. Lansezi un background process pentru a trimite system metrics. Verifici dashboard-ul mai târziu și jumătate din date lipsesc. Nu a fost aruncată nicio eroare. Codul tău s-a oprit silențios în mijlocul execuției. Asta se întâmplă pentru că ai tratat background job-ul ca pe un fire-and-forget. Astăzi vorbim despre scheduling cu Tasks și de ce trebuie să ții mereu de lucrurile pe care le creezi. Când ai o coroutine pe care vrei să o rulezi concurent cu alt cod, folosești funcția create task din asyncio. Pasezi coroutine-ul în această funcție, iar asyncio îl împachetează într-un obiect Task. Asta îi spune event loop-ului să facă schedule la task pentru execuție. Funcția îți returnează imediat noul obiect Task, permițând programului tău principal să ruleze în continuare, în timp ce task-ul operează în background. Mulți developeri apelează create task și ignoră valoarea returnată. Asta este o capcană uriașă. Iată ideea de bază. Event loop-ul din asyncio menține doar weak references către task-urile pe care le rulează. Loop-ul în sine nu îți protejează task-ul de garbage collector-ul din Python. Dacă nu atribui obiectul Task returnat unei variabile sau nu îl stochezi într-o structură de date, garbage collector-ul va observa la un moment dat că nu există hard references. Când se întâmplă asta, Python distruge obiectul Task. Nu îi pasă dacă coroutine-ul este fix în mijlocul execuției unui database query sau dacă așteaptă un network response. Task-ul pur și simplu dispare. Gândește-te la o funcție async numită ship metrics. Aceasta formatează un data payload și trimite un HTTP request către un server extern. Apelezi create task și pasezi ship metrics, dar nu atribui rezultatul la nimic. Task-ul începe să ruleze. Formatează payload-ul. Apoi ajunge la network call și se pune pe pauză pentru a aștepta o conexiune. În timp ce este pe pauză, garbage collector-ul rulează. Numărul de strong references este zero. Task-ul este distrus. Serverul nu primește niciodată payload-ul, iar aplicația ta nu loghează nicio eroare, pentru că execuția pur și simplu a încetat să existe. Pentru a preveni asta, trebuie să păstrezi mereu o strong reference către task-urile cărora le faci schedule. Dacă creezi un singur task, atribuie-l unei variabile. Dacă faci schedule la mai multe background tasks într-un loop, adaugă-le într-un set standard din Python sau într-o listă. Cât timp acel set există în memorie, există și acele strong references, iar garbage collector-ul îți va lăsa task-urile care rulează în pace. Apoi, poți folosi un callback pentru a elimina task-ul din set odată ce este gata. Funcția create task acceptă, de asemenea, câteva argumente opționale. Poți pasa un string parametrului name, care atribuie un identificator specific task-ului. Asta este foarte recomandat pentru debugging, deoarece face mult mai ușoară depistarea operațiunii specifice care a eșuat, dacă este aruncată o exception mai târziu. De asemenea, poți pasa un argument context pentru a stabili o stare specifică a variabilei de context pentru task. Tratarea operațiunilor de background ca fire-and-forget te va arde până la urmă cu silent failures. Dacă îi ceri event loop-ului să ruleze ceva, trebuie să păstrezi o hard reference către obiectul rezultat până când treaba este complet finalizată. Mulțumesc pentru ascultare, happy coding tuturor!
5

Structured Concurrency cu TaskGroups

3m 25s

Stăpânește structured concurrency. Înțelege cum asyncio.TaskGroup gestionează în siguranță multiple operațiuni concurente și asigură o oprire curată atunci când apar excepții.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 5 din 20. Înainte de Python 3.11, lansarea mai multor task-uri concurente era ușoară, dar gestionarea lor în siguranță atunci când unul crăpa era notoriu de dificilă. Te trezeai adesea cu background task-uri orfane care iroseau resurse în liniște. Soluția pentru acest haos este concurența structurată folosind TaskGroup. Un TaskGroup este un context manager asincron. Lumea îl confundă uneori cu o listă standard de task-uri, dar este mult mai strict. Oferă garanții puternice de siguranță despre cum încep și se termină task-urile. Impune regula că o rutină părinte nu se poate termina până când toate operațiunile child nu sunt fie finalizate, fie anulate curat. Îl folosești deschizând un bloc async with. În interiorul acelui bloc, apelezi metoda create task direct pe obiectul group pentru a-ți porni operațiunile concurente. Nu arunci aceste task-uri într-un array standard și nu le dai await manual. În schimb, când codul ajunge la finalul blocului async with, TaskGroup-ul pune pauză implicit. Așteaptă fix acolo până când fiecare task pornit se termină. Blocul pur și simplu nu va face exit mai devreme. Iată ideea cheie. Adevărata putere a unui TaskGroup stă în modul în care gestionează eșecurile. Cu tool-uri legacy precum gather, dacă porneai mai multe task-uri și unul arunca o eroare, celelalte continuau să ruleze în background. Trebuia să scrii o logică complexă de error handling pentru a găsi supraviețuitorii și a-i omorî. Un TaskGroup gestionează asta automat. Ia scenariul unui web scraper care face fetch simultan la trei endpoint-uri API distincte. Ai nevoie de date despre utilizatori, postări recente și alerte de sistem. Deschizi un TaskGroup și pornești trei task-uri. Toate încep să ruleze concurent prin rețea. La jumătatea operațiunii, endpoint-ul de postări recente dă timeout și aruncă o eroare de conexiune. TaskGroup-ul detectează imediat acest eșec. Interceptează eroarea și trimite automat un semnal de anulare către task-ul de date despre utilizatori și task-ul de alerte de sistem. Curăță acele operațiuni pending, astfel încât să nu continue să consume bandwidth de rețea sau memorie. Task-urile rămase aruncă intern o eroare de tip cancelled, confirmând oprirea. Odată ce toate task-urile rămase sunt oprite în siguranță, TaskGroup-ul grupează eroarea de conexiune originală într-o nouă structură numită ExceptionGroup și o aruncă în afara blocului de context. Acest comportament face codul tău asincron complet previzibil. Dacă execuția trece cu succes de bloc, știi sigur că absolut fiecare task a reușit. Dacă blocul aruncă un ExceptionGroup, știi că eșecul a fost prins și că tot restul a fost oprit corect. Nu lași niciodată task-uri rogue să ruleze în background. Dacă ai nevoie de rezultatele task-urilor reușite, le poți recupera direct din obiectele task pe care le-ai creat, cu condiția ca acestea să se fi finalizat înainte de producerea eșecului. Prin legarea task-urilor de un bloc strict de lifecycle, TaskGroup-urile garantează că operațiunile concurente intră și ies din aplicația ta ca o singură unitate coordonată. Asta e tot pentru azi. Mersi că m-ai ascultat — du-te și construiește ceva cool.
6

Anularea Task-urilor și Timeouts

4m 13s

Explorează mecanismele de abandonare a operațiunilor. Află de ce este declanșată asyncio.CancelledError, cum să o gestionezi într-un bloc finally și de ce nu ar trebui să o ignori niciodată.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 6 din 20. Ai scris un error handler robust, care prinde toate excepțiile generice din workerul tău async. Dar acum, la shutdown, event loop-ul tău se blochează cu task-uri zombie care refuză să moară. Plasa ta de siguranță, de fapt, le ține captive în viață. Exact asta acoperim astăzi: anularea task-urilor și timeout-urile. Când trebuie să oprești un task care rulează, îi apelezi metoda cancel. Asta nu termină instantaneu task-ul, așa cum ai omorî un proces de sistem. În schimb, asyncio cere oprirea prin injectarea unei erori, mai exact un asyncio CancelledError, în task. Această eroare primește raise exact la punctul de await curent sau următor al task-ului. Corutina face apoi unwind la stack, exact așa cum ar face pentru orice eroare standard din Python. Acest mecanism este fundamentul și pentru timeout-uri. Când faci wrap la un task într-o funcție de timeout și timer-ul expiră, event loop-ul nu oprește task-ul în mod magic. Pur și simplu apelează cancel pe acel task. Task-ul primește CancelledError la următorul său await, face unwind la state și, în cele din urmă, îi spune wrapper-ului de timeout că s-a oprit. Abia atunci wrapper-ul de timeout îți dă raise la un TimeoutError. Iată ideea cheie. Începând cu Python 3.8, CancelledError moștenește direct de la BaseException, nu de la clasa standard Exception. Această alegere de design previne o greșeală specifică, catastrofală. Dezvoltatorii fac în mod obișnuit wrap la operațiunile de rețea sau de fișiere în blocuri try și except care prind clase generice Exception pentru a preveni un crash. Dacă CancelledError ar fi un Exception standard, acele blocuri ar prinde semnalul de anulare. Task-ul probabil ar face log la un warning, ar înghiți semnalul și ar continua să se execute ca un zombie. Prin mutarea CancelledError mai sus în ierarhie la BaseException, Python garantează că error handlerele tale de zi cu zi nu vor intercepta accidental un request de anulare. Deci, cum gestionezi în siguranță state-ul atunci când un task este anulat? Te bazezi pe structura try și finally. Ia în considerare un server web care procesează un request HTTP primit. Utilizatorul cere un raport masiv, dar apoi își închide fereastra browserului. Serverul detectează deconectarea și anulează task-ul request-ului. În codul tău, în acel moment faci await la un query lung către baza de date. Acel await dă brusc raise la un CancelledError. Pentru că ai pus interacțiunea cu baza de date într-un bloc try, execuția sare instantaneu la blocul tău finally. Folosești acel bloc finally pentru a face un roll back curat la tranzacția în așteptare și a returna conexiunea la baza de date în pool. Odată ce blocul finally se termină, CancelledError continuă să facă bubble up, terminând cu succes task-ul. Uneori, un bloc finally nu este suficient. Dacă trebuie neapărat să faci un cleanup asincron, cum ar fi trimiterea unui request de rețea către un microserviciu remote pentru a anunța anularea, poți prinde explicit CancelledError. Dar dacă faci asta, trebuie să dai explicit re-raise la exact acea eroare la sfârșitul blocului tău except. Dacă nu îi dai re-raise, strici mecanica internă din asyncio. Task-ul va părea că s-a terminat cu succes în loc să fie anulat, ceea ce corupe state-ul aplicației tale și strică structured concurrency. Regula de reținut este că anularea este un request cooperativ, nu o comandă de kill forțată, și se bazează în întregime pe excepții care fac bubble up neatinse. Dacă vrei să susții emisiunea, poți căuta DevStoriesEU pe Patreon. Asta e tot pentru acest episod. Mulțumesc că ai ascultat și continuă să construiești!
7

Cedarea controlului cu Sleep

4m 06s

Înțelege adevăratul scop al asyncio.sleep(0). Descoperă cum cedarea controlului previne ca buclele cu consum mare de CPU să blocheze event loop-ul și să înghețe aplicația.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 7 din 20. Uneori, secretul pentru a menține serverul de rețea responsive este să le spui celor mai grele task-uri să intre în sleep pentru exact zero secunde. Dacă o funcție nu face pauză niciodată, întreaga ta aplicație nu mai ascultă lumea exterioară. Pentru a rezolva asta, folosești yielding control cu sleep. În framework-ul asyncio, event loop-ul rulează exact un singur task pe rând. Se bazează în întregime pe multitasking cooperativ. Un task rulează continuu până când atinge un keyword await, care acționează ca un checkpoint pentru a preda controlul execuției înapoi către event loop. Dacă scrii o funcție async care conține o operațiune pur CPU-bound, creezi un bottleneck. Gândește-te la parsarea unui JSON payload masiv sau la transformarea a mii de string-uri. Nu există puncte de await naturale într-un loop standard de procesare a datelor. Pentru că task-ul nu face yield niciodată, event loop-ul rămâne blocat. Orice network requests primite, răspunsuri de la baza de date sau health checks stau pur și simplu într-o coadă, rămânând fără resurse în timp ce așteaptă ca loop-ul tău să se termine. Metoda nativă de a rezolva asta este să predai manual controlul înapoi către event loop. Faci asta folosind un idiom specific: dând await la asyncio dot sleep cu un argument zero. La prima vedere, un sleep de zero secunde pare o operațiune inutilă. De ce să ceri sistemului să nu aștepte deloc? Iată ideea cheie. Un sleep de zero secunde nu are legătură cu trecerea timpului. Este un semnal explicit către event loop. Când dai await la un sleep de zero, corutina curentă este imediat suspendată. Event loop-ul preia controlul, plasează task-ul tău suspendat la finalul cozii de runnable și verifică dacă alte task-uri programate sunt gata de execuție. Dacă un network handler din background așteaptă să confirme o conexiune primită, îi vine rândul. Odată ce celelalte task-uri ating propriile lor puncte de await sau se termină, task-ul tău inițial ajunge înapoi în fața cozii și se reia exact de unde a rămas. Să aplicăm asta la un scenariu concret. Scrii o funcție async pentru a procesa milioane de înregistrări dintr-un fișier JSON. Dacă rulezi un while loop cap-coadă, serverul tău va părea mort. În schimb, introduci o variabilă counter. În interiorul loop-ului, procesezi o înregistrare și incrementezi counter-ul. Apoi adaugi o condiție simplă. Dacă counter-ul indică faptul că au trecut o sută de iterații, dai await la asyncio dot sleep zero. Asta împarte calculul masiv în chunk-uri gestionabile. Loop-ul procesează o sută de înregistrări, face un pas în spate pentru a lăsa serverul să răspundă la ping-uri sau să accepte date noi, apoi reia parsarea următoarelor o sută. Numărul de iterații dintre yield-uri este un parametru pe care trebuie să-l ajustezi. Să faci yield la fiecare iterație adaugă prea mult overhead, deoarece suspendarea și reluarea unei corutine are un mic cost computațional. Să faci yield la fiecare zece mii de iterații ar putea totuși să blocheze event loop-ul pentru prea mult timp. O sută este un punct de plecare rezonabil pentru a lăsa loop-ul să respire. Forțarea unui sleep de zero secunde este cea mai simplă modalitate de a-ți menține aplicația cooperativă, asigurându-te că un singur loop greoi nu lasă niciodată fără resurse restul sistemului. Mulțumesc pentru audiție, happy coding tuturor!
8

Sincronizare: Locks și Mutexes

4m 32s

Previne race conditions în codul async. Explorăm asyncio.Lock, discutăm natura sa non-thread-safe și arătăm de ce lock-urile de threading îți vor îngheța event loop-ul.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 8 din 20. Arunci un thread lock standard în aplicația ta async pentru a proteja o resursă partajată și, dintr-o dată, întregul tău event loop îngheață complet. Lock-ul și-a făcut treaba, dar a oprit tot restul în acest proces. Pentru a rezolva asta fără să blochezi loop-ul, folosim Sincronizare: Locks și Mutex-uri. Un lock asyncio, adesea numit mutex, garantează acces exclusiv la o resursă partajată între task-urile async. În primul rând, trebuie să clarificăm o confuzie comună. Nu poți folosi un thread lock standard din modulul threading din Python în interiorul unei aplicații async. Un lock de threading funcționează la nivelul sistemului de operare. Dacă nu poate face acquire pe lock, pune pe pauză întregul thread. Pentru că asyncio rulează mai multe task-uri în mod cooperativ pe un singur thread, blocarea acelui thread înseamnă că event loop-ul se oprește. Niciun network request nu se execută, niciun timer nu ticăie. Totul îngheață. Un lock asyncio rezolvă asta fiind task-safe, nu thread-safe. Când un task asyncio încearcă să facă acquire pe un mutex blocat, nu blochează thread-ul. În schimb, se suspendă și cedează controlul înapoi event loop-ului. Asta permite altor task-uri fără legătură să își continue treaba în timp ce primul task așteaptă la rând. Să ancorăm asta într-un scenariu concret. Ai o aplicație cu zeci de task-uri async care fac API calls externe. Token-ul tău OAuth expiră. Două task-uri diferite observă token-ul expirat în exact aceeași milisecundă. Fără sincronizare, ambele task-uri vor trimite independent un request către serverul de autentificare pentru a face refresh la token. Această muncă redundantă poate declanșa rate limits sau poate invalida imediat primul token din cauza politicilor stricte de rotație. Pentru a preveni acest race condition, creezi un singur lock asyncio atunci când îți inițializezi aplicația. Acest obiect lock este pasat sau partajat între toate task-urile tale API. Acum, uită-te la flow. Atât Task-ul A, cât și Task-ul B detectează token-ul expirat. Task-ul A ajunge primul la blocul de sincronizare și face await pe lock. Îl obține cu succes. Task-ul B ajunge o fracțiune de secundă mai târziu și face await pe același lock. Pentru că Task-ul A îl deține, Task-ul B intră în sleep, lăsând event loop-ul să se ocupe de alte treburi. Când mai multe task-uri așteaptă același lock, asyncio le așază la rând. Odată ce lock-ul este eliberat, event loop-ul trezește primul task de la rând. Task-ul A solicită în siguranță noul token, actualizează variabila partajată a token-ului și eliberează lock-ul. În acel moment, event loop-ul trezește Task-ul B. Task-ul B obține în cele din urmă lock-ul. Totuși, înainte de a face un network call, Task-ul B verifică din nou token-ul. Vede că token-ul este deja valid, sare peste pasul de refresh, eliberează lock-ul și continuă cu API request-ul său principal. Cel mai sigur mod de a implementa această logică este folosind un context manager async. În codul tău, scrii un statement async with urmat de obiectul lock. Când execuția intră în acest bloc, așteaptă acces exclusiv. Când execuția iese din bloc, fie în mod normal, fie pentru că o eroare a făcut crash la task, eliberează automat lock-ul. Nu este nevoie să apelezi manual metodele acquire sau release, ceea ce elimină riscul de a lăsa accidental un lock activat pentru totdeauna. Iată ideea cheie. Un lock asyncio nu îți protejează state-ul de alte thread-uri ale sistemului de operare; îți protejează state-ul de propriile tale task-uri concurente care se calcă pe picioare în timp ce fac await pe alte operațiuni. Mersi că ai stat pe aici. Sper că ai învățat ceva nou.
9

Coordonarea stării cu Events

3m 38s

Învață să transmiți semnale către mai multe task-uri aflate în așteptare. Explicăm cum asyncio.Event și asyncio.Condition înlocuiesc elegant buclele ineficiente de interogare.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 9 din 20. Ai cincizeci de task-uri care așteaptă să se conecteze la o bază de date. Cu siguranță nu vrei să facă polling într-o buclă, irosind cicluri de CPU în timp ce verifică dacă e gata conexiunea. Ai nevoie de un singur semnal de broadcast care să le spună tuturor să înceapă interogarea exact în același moment. Exact de asta se ocupă coordonarea stării cu Event-uri și Condition-uri. Un Event din asyncio gestionează un simplu flag boolean intern. Acesta pornește ca false. Înainte să ne uităm la flux, hai să clarificăm o confuzie comună între Event-uri și Lock-uri. Un Lock acordă acces exclusiv la exact un singur task pe rând, ținându-le pe celelalte pe loc. Un Event face opusul. Notifică simultan mai multe task-uri aflate în așteptare, permițându-le tuturor să continue în același timp. Gândește-te la scenariul cu conexiunea la baza de date. Task-ul tău de background lucrează pentru a stabili conexiunea. Între timp, cele cincizeci de task-uri worker ajung într-un punct în care au nevoie de baza de date. Fiecare worker apelează metoda wait pe obiectul tău Event partajat. Pentru că flag-ul intern este false, toate cele cincizeci de task-uri se suspendă. Stau inactive. În cele din urmă, task-ul de background reușește și apelează metoda set pe Event. Flag-ul devine true. Instantaneu, toate cele cincizeci de task-uri worker suspendate se trezesc și își reiau execuția. Dacă trebuie să închizi conexiunea mai târziu, poți apela metoda clear pe Event. Flag-ul revine la false, iar orice apeluri viitoare către wait se vor bloca din nou. De asemenea, poți verifica starea curentă a flag-ului în orice moment apelând metoda is set, care returnează true sau false fără să blocheze task-ul. Asta acoperă semnalele simple de broadcast. Uneori, un singur flag boolean nu este de ajuns. S-ar putea să ai mai multe task-uri care trebuie să aștepte ca o resursă partajată să ajungă la o anumită stare complexă și au nevoie de acces exclusiv pentru a verifica sau modifica în siguranță acea stare. Aici intervine Condition din asyncio. Un Condition este construit în jurul unui Lock subiacent. Pentru a face orice cu un Condition, un task trebuie mai întâi să îi facă acquire. Odată ce i-a făcut acquire, task-ul verifică starea partajată. Dacă starea nu este cea de care are nevoie task-ul, task-ul apelează metoda wait pe Condition. Aici e ideea de bază. Apelarea metodei wait pe un Condition face două lucruri deodată: eliberează Lock-ul subiacent, permițând altor task-uri să acceseze starea, și suspendă task-ul curent. În timp ce acel task este suspendat, un alt task poate să facă acquire pe Condition, să schimbe starea partajată și apoi să apeleze metoda notify. Metoda notify primește un argument care specifică exact câte task-uri în așteptare să fie trezite, valoarea default fiind unu. Poți, de asemenea, să apelezi notify all pentru a le trezi pe toate deodată. Când un task suspendat se trezește, nu începe pur și simplu să ruleze imediat. Trebuie să aștepte să facă din nou acquire pe Lock-ul subiacent înainte ca metoda wait să returneze. Pentru că un alt task ar putea să preia Lock-ul și să schimbe starea înainte ca task-ul trezit să-și primească rândul, apelul wait este aproape întotdeauna plasat într-o buclă while care verifică încontinuu starea dorită. Odată ce are Lock-ul înapoi și starea este corectă, poate continua în siguranță și, în cele din urmă, poate elibera acel Condition. Când decizi între cele două, ține minte că un Event este un simplu broadcast care le spune task-urilor că a avut loc o acțiune singulară, în timp ce un Condition permite task-urilor să aștepte în siguranță o schimbare complexă de stare fără să facă polling constant pe o resursă blocată. Mulțumesc că ai petrecut câteva minute cu mine. Până data viitoare, numai bine.
10

Limitarea concurenței cu Semaphores

3m 59s

Protejează resursele fragile și previne banările din cauza rate-limiting-ului. Descoperă cum asyncio.Semaphore limitează execuția concurentă fără a-ți bloca arhitectura.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 10 din 20. Trimiterea a zece mii de request-uri asincrone către un API third-party fragil este o modalitate extrem de eficientă de a-ți bloca permanent adresa IP. Codul tău rulează impecabil, dar serverul de la celălalt capăt se prăbușește sub spike-ul brusc de trafic. Pentru a proteja serviciile externe și propriul acces, trebuie să faci throttle aplicației tale. Acest scut este limitarea concurenței folosind Semaphores. E bine să clarificăm din start o concepție greșită comună. Un Semaphore nu este un rate limiter. Nu limitează câte request-uri face programul tău pe secundă. În schimb, limitează operațiunile concurente. Controlează strict câte task-uri pot executa un anumit bloc de operațiuni de rețea sau pe fișiere în exact același moment. Dacă un task își termină apelul API în zece milisecunde, acel slot se eliberează imediat pentru următorul task la rând. Ai putea totuși să procesezi sute de operațiuni pe secundă, cu condiția ca nu mai mult de limita permisă să fie in flight simultan. Un Semaphore din asyncio gestionează un simplu counter intern. Când creezi obiectul Semaphore, oferi o valoare inițială. Să luăm scenariul limitării request-urilor HTTP outgoing către un API extern delicat la exact zece conexiuni concurente. Îți inițializezi obiectul Semaphore cu o valoare de zece. Înainte ca orice task asincron să facă un network request, acesta trebuie să facă acquire pe Semaphore. Această acțiune scade counter-ul intern cu unu. Când network request-ul se termină, task-ul face release la Semaphore, crescând counter-ul la loc cu unu. Iată ideea cheie. Dacă zece task-uri au făcut deja acquire pe Semaphore, counter-ul stă la zero. Când al unsprezecelea task încearcă să facă acquire, acel task este suspendat. Metoda acquire blochează progresul până când unul din primele zece task-uri se termină și face release. Acest lock numeric simplu te asigură că nu depășești niciodată hard limit-ul de zece conexiuni active. În utilizarea reală, ar trebui să apelezi rareori manual metodele acquire și release. În schimb, folosești obiectul Semaphore ca un context manager asincron. Împachetând request-ul HTTP într-un async with statement, Python garantează că se face release la Semaphore când se iese din blocul de cod. Acest release are loc chiar dacă API-ul dă timeout, pică conexiunea sau aruncă un unhandled exception. Dacă încerci să faci release manual și o eroare sare peste apelul tău de release, acel slot de concurență este pierdut pentru totdeauna. Dacă pierzi toate cele zece slot-uri din cauza unor erori tranzitorii de rețea, întregul tău program intră în deadlock în tăcere. Există un pericol subtil cu un Semaphore standard. Dacă o eroare de logică din codul tău face ca un task să dea release la Semaphore de mai multe ori decât a dat acquire, counter-ul intern va crește dincolo de limita ta inițială de zece. Dintr-o dată, scutul tău de concurență este spart și, fără să știi, trimiți douăsprezece sau cincisprezece request-uri simultane. Pentru a preveni asta, ar trebui să folosești un Bounded Semaphore din asyncio. Un Bounded Semaphore se comportă exact ca un Semaphore standard, dar ține evidența valorii inițiale pe care i-ai dat-o. Dacă un task rebel încearcă să dea release la Semaphore peste acea limită de start, Bounded Semaphore aruncă imediat un ValueError. Dă crash devreme și zgomotos, în loc să copleșească în tăcere API-ul extern. Folosește mereu by default un Bounded Semaphore, cu excepția cazului în care ai un motiv arhitectural foarte specific pentru a-ți crește dinamic limitele de concurență. Bounded Semaphores prind erorile logice de release în momentul în care apar, menținând stricte limitele de conexiune la API și sistemele tale rulând previzibil. Asta e tot pentru acest episod. Mersi că m-ai ascultat și continuă să construiești!
11

Fluxuri de lucru Producer-Consumer

3m 40s

Decuplează în siguranță producer-ii rapizi de consumer-ii lenți. Explorează asyncio.Queue, semnalizarea finalizării task-urilor și noile mecanisme de oprire pentru queues.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 11 din 20. Ai un web server async care gestionează mii de request-uri pe secundă și, pentru fiecare request, trebuie să scrii un log pe disk. Dacă serverul tău așteaptă ca acea scriere pe disk să se termine înainte să răspundă, performanța ta se prăbușește. Cea mai fiabilă metodă de a decupla producerii rapizi de consumerii lenți în Python async este integrată direct în standard library. Astăzi ne uităm la workflow-urile Producer-Consumer folosind queues din asyncio. Unii developeri care vin din programarea multi-threaded presupun că trebuie să pună acest queue în locks pentru a preveni race conditions. Nu trebuie să faci asta. Acest queue din asyncio este conceput special pentru task-uri concurente care rulează pe un singur event loop. Este inerent safe pentru acele task-uri. Lasă acele queues thread-safe din modulul standard queue pentru threading; folosește versiunea asyncio pentru async. Gândește-te la queue ca la un pipe. La un capăt, ai produceri care fac push la iteme. La celălalt capăt, ai consumeri care fac pull la iteme. Hai să folosim acel scenariu de logging. Acel request handler web al tău este producerul. Acesta primește un request, formatează un eveniment de log și apelează metoda async put pe queue. Dacă setezi un max size când creezi acel queue, primești automat backpressure. Când queue-ul este plin, dacă dai await pe metoda put, producerul este pus pe pauză până când se eliberează spațiu. Asta previne ca un spike uriaș de trafic să îți epuizeze memoria sistemului. Pe cealaltă parte a pipe-ului, ai un background task separat care acționează ca un consumer. Acest task rulează într-un loop continuu. Apelează metoda async get pe queue. Dacă queue-ul este empty, consumerul intră în sleep în siguranță. Event loop-ul îl trezește exact în momentul în care un producer aruncă un nou eveniment de log în pipe. Consumerul preia evenimentul, îl scrie pe disk și apoi semnalează că acel job specific este complet apelând o metodă numită task done. Gestionarea acestui flow în timpul procesului de teardown al aplicației este critică. Dacă trebuie să dai shut down la web server gracefully, vrei să te asiguri că toate evenimentele de log din queue sunt scrise efectiv pe disk. Acest queue are o metodă numită join. Când dai await pe join, programul tău se blochează până când numărul de apeluri task done se potrivește exact cu numărul de iteme puse inițial în queue. Asta garantează că fiecare bucată de work a fost procesată complet. Iată ideea cheie. Python 3.13 a introdus o nouă metodă pe queue numită shutdown. Anterior, oprirea clean a unui loop producer-consumer necesita pasarea unor valori sentinel speciale, cum ar fi injectarea unui obiect None în queue, doar pentru a-i spune consumerului să iasă din loop-ul său. Acum, poți pur și simplu să apelezi shutdown. Când faci asta, orice task blocat în acel moment, care așteaptă să facă put sau get pe un item, este lovit imediat de o excepție QueueShutDown. Prinzi această excepție în worker task-urile tale, îți cureți resursele și ieși clean, fără nicio logică sentinel fragilă. Când proiectezi un sistem asyncio, ține minte că aceste queues nu sunt doar data structures; sunt mecanisme de flow control care gestionează nativ backpressure-ul, menținându-ți memory footprint-ul stabil chiar și atunci când producerii depășesc cu mult consumerii. Asta e tot pentru acest episod. Mersi că m-ai ascultat și continuă să construiești!
12

Rețelistică High-Level cu Streams

4m 02s

Aprofundează IO Streams de nivel înalt. Discutăm despre StreamReader, StreamWriter și de ce omiterea await writer.drain() poate distruge în tăcere memoria serverului tău.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 12 din 20. Trimiți date printr-o conexiune de rețea, iar loop-ul tău arată perfect în regulă. Dar, în culise, aplicația ta consumă în tăcere gigabytes de memorie până când sistemul o oprește. Problema se reduce de obicei la o linie de cod lipsă care gestionează flow control-ul. De aceea, astăzi ne uităm la networking high-level cu streams. Asyncio oferă un API high-level pentru a lucra cu conexiuni de rețea fără a atinge raw sockets sau protocoale de transport low-level. Pentru a stabili o conexiune TCP, folosești o funcție top-level numită open_connection. Îi pasezi un string pentru host și un integer pentru port. Aceasta returnează imediat un tuple de două obiecte: un StreamReader și un StreamWriter. Dacă construiești un server în loc de un client, folosești start_server. Oferi o funcție de callback, un host și un port. De fiecare dată când un client nou se conectează, asyncio îți declanșează callback-ul, pasându-i un reader și un writer dedicate pentru acea conexiune specifică a clientului. StreamReader este interfața ta pentru primirea datelor. Acesta oferă metode asincrone pentru a extrage bytes din rețea. Poți citi un număr maxim specific de bytes folosind metoda read. Dacă faci parsing pe protocoale line-based, poți citi până la un separator specific, cum ar fi un newline, folosind metoda readuntil. Dacă protocolul tău necesită un header cu dimensiune fixă, poți folosi readexactly, care va aștepta până când sosește exact acel număr de bytes. Deoarece toate aceste operațiuni depind de traficul și latența rețelei, ele pun coroutine-ul în pauză, ceea ce înseamnă că trebuie să le dai await. Acum, a doua parte a acestui proces este StreamWriter. Acest obiect se ocupă de trimiterea datelor înapoi. Folosești metoda write pentru a împinge bytes în stream. Iată ideea cheie. Metoda write este o funcție obișnuită, nu una asincronă. Nu îi dai await. Când apelezi write, nu pui instantaneu datele pe rețea. Pur și simplu pui datele într-un buffer asyncio intern. Event loop-ul din spate încearcă să dea flush la acest buffer către rețea, în background. Acest buffer este locul unde developerii dau de probleme. Gândește-te la un client TCP care trimite un payload masiv al unui fișier către un server lent. Dacă pui apelul write într-un tight loop care citește chunks de pe un disk local, Python va citi fișierul mult mai repede decât îl poate transmite rețeaua. Deoarece write nu îți blochează codul, loop-ul tău continuă să ruleze. Bufferul intern absoarbe întregul fișier, consumând toată memoria disponibilă a sistemului. Aici intervine backpressure-ul. Pentru a gestiona flow control-ul, trebuie să asociezi apelurile write cu metoda drain. Metoda drain este asincronă, ceea ce înseamnă că îi dai await. Când dai await la drain, îi spui event loop-ului să îți pună coroutine-ul în pauză dacă bufferul intern a depășit high-water mark-ul. Codul tău așteaptă până când procesul din background trimite suficiente date prin rețea pentru a micșora bufferul la o dimensiune sigură. Rețeaua are timp să recupereze, bufferul se golește, iar utilizarea memoriei rămâne constantă. După ce ai terminat de trimis fișierul, apelezi metoda close pe writer. La fel ca write, close nu este o funcție asincronă. Pentru a te asigura că se închide curat conexiunea și că toți bytes finali primesc flush înainte ca programul să continue, urmezi procesul dând await metodei wait_closed. StreamWriter face ca scrierea într-o rețea să pară instantanee, dar fizica se aplică în continuare. Dă mereu await la drain după ce scrii, pentru a te asigura că aplicația ta respectă viteza reală a conexiunii la rețea. Mulțumesc pentru audiție, happy coding tuturor!
13

Construirea serverelor Async

4m 01s

Construiește servere de rețea extrem de concurente. Învață cum asyncio.start_server abstractizează conexiunile clienților, generând un task izolat pentru fiecare peer.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 13 din 20. Construirea unui server TCP cu concurență ridicată în Python înseamnă de obicei să te lupți cu thread pools sau cu configurații complexe de event loop. De fapt, poți gestiona mii de conexiuni în mai puțin de zece linii de cod. Exact asta acoperim astăzi prin construirea de servere async cu asyncio streams. Fundamentul unui server de rețea în asyncio este o funcție numită start_server. Îi transmiți trei lucruri: un callback, o adresă IP și un port. Când dai await pe start_server, acesta face bind pe acea adresă și începe să asculte conexiunile TCP primite pe interfața de rețea pe care ai specificat-o. Dezvoltatorii presupun adesea că trebuie să intercepteze manual aceste conexiuni primite și să scrie cod boilerplate pentru a le trimite către worker threads sau background tasks custom. Asta este complet inutil. Framework-ul se ocupă de concurență pentru tine. De fiecare dată când un client nou se conectează la portul tău, start_server face spawn automat la un task asyncio complet nou, dedicat în întregime acelui client specific. Gândește-te că construiești un server simplu pentru un chat room. Când primul tău utilizator se conectează, start_server îți declanșează funcția de callback și îi pasează două obiecte: un stream reader și un stream writer. Dacă încă cincizeci de utilizatori se conectează simultan, cincizeci de task-uri separate pornesc instantaneu pentru a rula exact aceeași funcție de callback. Fiecare task primește propria pereche izolată de reader și writer. În interiorul funcției tale de callback, scrii logica ca și cum ai vorbi cu o singură persoană la un moment dat. Folosești obiectul reader pentru a asculta mesajele primite. Dai await pe o metodă read de pe reader, specificând un număr maxim de bytes pe care vrei să îi accepți, cum ar fi o sută de bytes. Reader-ul îți oferă bytes raw din rețea, pe care îi decodezi într-un string de text standard. Pentru a răspunde clientului, inversezi procesul. Encodezi string-ul de răspuns înapoi în bytes și îl pasezi direct obiectului writer. Iată ideea cheie. Pasarea datelor către writer nu este o operațiune asincronă, dar să te asiguri că datele părăsesc efectiv mașina fizică, este. După ce oferi date writer-ului, trebuie să dai await pe metoda drain a writer-ului. Draining-ul pune în pauză task-ul client curent până când network buffer-ul sistemului de operare are suficient spațiu liber pentru a împinge acei bytes prin cablu. Acest pas este esențial deoarece împiedică serverul tău să consume toată memoria disponibilă dacă un client are o conexiune la rețea lentă. Când conversația se termină, sau dacă clientul se deconectează, îi spui writer-ului să dea close. Apoi dai await pe metoda wait_closed a writer-ului pentru a te asigura că toți acei bytes finali sunt transmiși și că socket-ul subiacent se închide curat. Înapoi în funcția ta principală de setup, start_server a returnat un obiect server. By default, serverul se oprește din ascultat dacă scriptul principal Python ajunge la sfârșitul instrucțiunilor sale. Pentru a menține chat room-ul deschis pe termen nelimitat, iei acel obiect server și dai await pe metoda sa serve_forever. Asta blochează task-ul principal asyncio într-un infinite loop, acceptând în liniște noi conexiuni și făcând spawn la noi task-uri de client în background. Adevărata putere a acestui design este că abstractizează complexitatea de networking. Scrii cod secvențial, simplu, pentru o singură conexiune izolată, iar event loop-ul îl scalează automat pe task-uri concurente. Dacă vrei să ajuți la susținerea emisiunii, poți căuta DevStoriesEU pe Patreon. Asta e tot pentru acest episod. Mulțumesc că m-ai ascultat și continuă să construiești!
14

Subprocesses Non-blocking

3m 36s

Rulează comenzi shell în mod asincron. Descoperă de ce folosirea modulului standard subprocess oprește event loop-ul și cum asyncio.create_subprocess_exec rezolvă acest lucru.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 14 din 20. Construiești un web API asincron, declanșezi o comandă standard de sistem în interiorul unui endpoint și, dintr-o dată, toate celelalte task-uri concurente paralizează instantaneu. Nimic nu se mișcă până când acea comandă de sistem nu se termină. Vinovatul este modulul standard Python subprocess, iar rezolvarea acestei probleme necesită subprocess-uri non-blocking. Apelarea unei funcții precum standardul subprocess dot run execută o comandă a sistemului de operare și așteaptă finalizarea acesteia. Într-o aplicație Python asincronă, event loop-ul rulează pe un singur thread. Când blochezi acel thread în așteptarea sistemului de operare, event loop-ul se oprește. Toate celelalte request-uri concurente către API-ul tău rămân blocate. Pentru a remedia acest lucru, asyncio oferă propriile funcții de subprocess concepute special pentru event loop. Instrumentul principal este asyncio dot create subprocess exec. Iată ideea cheie. Această funcție nu execută comanda direct în Python. Ea solicită sistemului de operare să creeze un child process, dar în loc să se blocheze așteptând rezultatul, cedează imediat controlul înapoi event loop-ului. API-ul tău gestionează alte request-uri în timp ce programul extern rulează. Ia scenariul unui web API care convertește fișiere video folosind FFmpeg. Vrei să declanșezi conversia și să faci stream la log-urile de output înapoi către utilizator, în timp real. În interiorul endpoint-ului tău async, apelezi create subprocess exec. Îi pasezi numele programului, FFmpeg, urmat de argumentele sale. Pentru a captura log-urile, îi spui funcției să ruteze standard output și standard error către pipe-uri asyncio. Funcția returnează un obiect Process din asyncio. Acest obiect reprezintă comanda de sistem de operare care rulează și îți oferă hook-uri async pentru a interacționa cu ea. Pentru că ai rutat output-urile către pipe-uri, obiectul Process le expune ca stream readers async. Citești log-urile FFmpeg iterând asincron peste stream-ul de standard error, deoarece FFmpeg face log de obicei acolo. Pentru fiecare linie produsă de procesul extern, loop-ul tău async se trezește, citește linia și îi face stream înapoi către utilizatorul web. În timp ce așteaptă următoarea linie, event loop-ul Python se întoarce imediat la servirea altor utilizatori. Obții log streaming în timp real fără să blochezi serverul. Dacă nu ai nevoie să faci stream la output linie cu linie, obiectul Process oferă și o metodă async communicate. Faci await pe communicate pentru a trimite date către standard input și pentru a citi toate datele din standard output și standard error deodată. Acest lucru menține loop-ul liber până când procesul extern se termină complet și returnează datele. Dacă ai gestionat stream-urile manual, ca în exemplul FFmpeg, în schimb faci await pe metoda wait de pe obiectul Process pentru a aștepta ca procesul să se termine și pentru a-i colecta exit code-ul. Event loop-ului nu îi pasă dacă sistemul de operare efectuează calculul propriu-zis; dacă codul tău Python așteaptă sincron ca sistemul de operare să răspundă, întreaga ta aplicație asincronă este complet blocată. Asta e tot pentru acest episod. Mulțumesc pentru audiție și continuă să construiești!
15

Futures: Puntea Low-Level

3m 56s

Descoperă baza instrucțiunilor await. Examinăm asyncio.Future, rolul său ca rezultat final și modul în care face legătura între codul legacy bazat pe callback-uri și sintaxa modernă.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 15 din 20. Scrii cod asincron curat și modern, dar în cele din urmă trebuie să interacționezi cu o librărie veche și încăpățânată care se bazează în întregime pe callback-uri. Nu poți da await direct pe un callback, ceea ce îți strică întregul flux asincron. Mecanismul care leagă aceste două lumi este Futures: Puntea low-level. Să clarificăm imediat o confuzie comună. Lumea confundă frecvent Task-urile și Future-urile. Un Task este o subclasă specifică a unui Future. Un Task face wrap la un coroutine și îl programează activ pe event loop, conducându-i execuția pas cu pas. Un Future nu rulează nimic. Nu are o logică de execuție proprie. Este pur și simplu un state container. Este o primitivă low-level care reprezintă rezultatul final al unei operații asincrone. Când scrii Python modern, aproape niciodată nu instanțiezi un Future direct. Event loop-ul le creează în spate. Dar când trebuie să faci wrap la cod legacy, bazat pe callback-uri, le construiești manual. Ia în considerare un scenariu în care folosești o librărie mai veche de protocol de rețea. Aceasta are o metodă request care primește o adresă de rețea, un success callback și un failure callback. Vrei ca funcția ta modernă async să apeleze pur și simplu await pe acest request. Iată cum acoperi acest decalaj. În interiorul funcției tale async, obții event loop-ul care rulează curent și îi ceri să creeze un nou obiect Future. În acest moment exact, Future-ul este într-o stare pending. Este gol și așteaptă. Apoi, scrii o mică funcție success callback. Când este declanșată, această funcție preia datele primite și apelează metoda set result pe Future-ul tău. De asemenea, scrii un error callback care apelează metoda set exception pe același Future. Pasezi ambele funcții în metoda legacy request și pornești apelul de rețea. În cele din urmă, dai await pe Future. Iată ideea cheie. Folosirea lui await pe un Future pending pune pe pauză coroutine-ul curent. Cedează controlul înapoi către event loop, permițând altor task-uri să ruleze. Codul tău rămâne înghețat la acel statement await. Între timp, clientul legacy își face operațiunile de network input și output în fundal. Când sosesc datele, clientul legacy îți declanșează success callback-ul. Callback-ul tău apelează set result pe Future. Future-ul trece imediat din starea pending în starea finished. Event loop-ul observă această schimbare de stare. Trezește coroutine-ul care aștepta acel Future, despachetează rezultatul stocat, iar funcția ta async își reia execuția exact ca și cum ar fi dat await pe un coroutine nativ. Dacă apelul de rețea a eșuat, error callback-ul tău setează o excepție pe Future în schimb. Când event loop-ul trezește coroutine-ul, aruncă exact acea excepție la linia cu await. Un Future are reguli stricte în privința stării. Poate ieși din starea pending o singură dată. Dacă un callback încearcă să apeleze set result pe un Future care este deja finished, Python aruncă o eroare de invalid state. De asemenea, poți da cancel manual unui Future. Dacă faci asta, intră într-o stare cancelled, iar orice coroutine care dă await pe el primește imediat o eroare asyncio Cancelled Error. Future-urile oferă liantul structural necesar între callback-urile event-driven și statement-urile await cu aspect procedural. Faptul că înțelegi că fiecare statement await pune în cele din urmă pe pauză execuția până când un Future low-level este marcat ca finished, îți oferă o claritate totală asupra modului în care Python asincron funcționează de fapt în spate. Asta e tot pentru acest episod. Îți mulțumesc pentru audiție și continuă să construiești!
16

Transports și Protocols

4m 23s

Privește sub capotă pentru a vedea cum asyncio comunică cu sistemul de operare. Înțelege relația 1:1, bazată pe callback-uri, dintre Transports (cum se mișcă octeții) și Protocols (ce înseamnă octeții).

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 16 din 20. Când folosești streams asyncio de nivel înalt, codul tău arată curat, secvențial și este awaited în siguranță. Dar sub aceste coroutines prietenoase se află un motor puternic optimizat, bazat pe callback-uri, care se ocupă de apelurile complicate de sistem de operare. Pentru a înțelege cum comunică aplicația ta Python cu o rețea, trebuie să te uiți la Transports și Protocols. Aceste două abstracții formează fundamentul de networking din asyncio. Ele funcționează mereu în pereche. Protocolul se ocupă de logica aplicației, decizând ce bytes să trimită și cum să interpreteze datele primite. Transportul se ocupă de partea mecanică. Nu-i pasă ce înseamnă datele tale sau cum sunt formatate. Singura lui treabă este să-și dea seama cum să împingă acei bytes pe rețea. Astăzi, ne concentrăm exclusiv pe layer-ul de transport. Gândește-te la ce se întâmplă când scrii direct într-un socket TCP non-blocking. Trebuie să întrebi sistemul de operare dacă socket-ul este gata. Trebuie să gestionezi scrierile parțiale dacă buffer-ul de rețea este plin. Trebuie să ții evidența care bytes s-au trimis efectiv și care trebuie reîncercați mai târziu. Un transport asyncio ascunde toată această complexitate. Acționează ca un wrapper opac în jurul raw socket-ului și al apelurilor de sistem de operare subiacente. În general, nu instanțiezi niciodată un transport manual. În schimb, apelezi o metodă de pe event loop pentru a crea o conexiune de rețea. Event loop-ul configurează socket-ul, creează transportul, îl leagă de protocolul tău și îți returnează perechea. Iată ideea cheie. Odată ce conexiunea este stabilită, transportul preia controlul asupra buffering-ului de input și output. Când protocolul tău vrea să trimită un mesaj, pur și simplu pasează un chunk de bytes către metoda write a transportului. Transportul nu îți blochează codul ca să aștepte rețeaua. El pune imediat acei bytes în propriul său buffer intern. Transportul lucrează apoi cu event loop-ul în background, declanșând apelurile de socket non-blocking către sistemul de operare. Dacă sistemul poate prelua doar jumătate din bytes acum, transportul păstrează restul și încearcă din nou la următoarea iterație a loop-ului. Aplicația ta nu trebuie niciodată să facă micromanagement pe acea coadă. Flow control-ul este integrat direct în acest mecanism. Dacă scrii date mai repede decât le poate trimite rețeaua, buffer-ul intern al transportului va începe să se umple. Odată ce atinge o limită desemnată, transportul declanșează un callback specific pe protocolul tău pentru a pune pe pauză scrierea. Când buffer-ul se golește în sfârșit, declanșează un alt callback pentru a relua. Pe partea de primire, transportul ascultă event loop-ul. Când sistemul de operare semnalează că au sosit bytes, transportul îi extrage din socket și îi trimite direct în protocol printr-un callback. Totul la acest nivel scăzut este bazat exclusiv pe callback-uri. Nu există awaitables aici. Transports oferă, de asemenea, metode standardizate pentru gestionarea ciclului de viață al conexiunii. Poți închide gracefully un transport, ceea ce îi spune să termine de trimis orice date din buffer înainte de a închide socket-ul în siguranță. Dacă lucrurile merg prost, poți apela o metodă abort pentru a întrerupe imediat conexiunea, eliminând tot ce a mai rămas în coadă. Și dacă protocolul tău trebuie să știe cu cine vorbește, transportul oferă o metodă de a solicita informații suplimentare, permițându-ți să te uiți dincolo de abstracție și să recuperezi adresa IP a socket-ului subiacent sau detalii despre peer. Abstracția de transport este cea care permite codului tău asyncio să rămână concentrat exclusiv pe logica datelor. Transports îți izolează aplicația de mecanica haotică a I/O-ului non-blocking; ele preiau raw bytes din protocolul tău și gestionează în tăcere buffering-ul, retry-urile și apelurile de socket ale sistemului de operare necesare pentru a-i muta prin rețea. Asta e tot pentru acest episod. Mulțumesc că m-ai ascultat și continuă să construiești!
17

Threading într-o lume Async

3m 26s

Creează o punte între lumile sincronă și asincronă. Învață cum să externalizezi în siguranță codul blocant greoi folosind executors și callback-uri thread-safe, fără a bloca loop-ul.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 17 din 20. Pui un background thread standard în web serverul tău async ca să gestionezi un task lent, și dintr-o dată aplicația ta începe să dea deadlock sau să arunce erori de state criptice. Să amesteci thread-uri standard cu un event loop async este o rețetă pentru dezastru, dacă nu folosești punțile thread-safe desemnate. Astăzi vorbim despre Threading într-o lume async. Regula principală din asyncio este că event loop-ul rulează într-un singur thread. Din această cauză, aproape toate obiectele asyncio nu sunt thread-safe. O greșeală comună este să pornești un background thread standard, să faci niște treabă, și apoi să încerci să dai resolve unui future async sau să programezi un callback direct din acel thread. Dacă atingi un obiect asyncio din alt thread decât cel care rulează event loop-ul, vei corupe state-ul loop-ului. Pentru a trimite un mesaj dintr-un background thread în event loop-ul tău, trebuie să folosești call soon threadsafe. Aceasta este o metodă chiar de pe loop. Îi dai funcția callback pe care vrei să o rulezi și argumentele. În loc să o execute imediat, background thread-ul tău pune acel callback într-un queue intern sigur. Event loop-ul principal verifică acest queue și execută callback-ul tău în siguranță în main thread, în timpul ciclului său normal. Aceasta este singura modalitate sigură prin care un thread extern poate interacționa cu event loop-ul. Acum, ia în considerare situația inversă. Rulezi event loop-ul tău async și trebuie să execuți o bucată de cod sincron, blocant. Un scenariu clasic este interogarea unui driver PostgreSQL lent și sincron, cum ar fi psycopg2. Dacă execuți un query de bază de date de cinci secunde direct în interiorul request handler-ului tău async, întregul tău web server se oprește. Event loop-ul nu poate procesa niciun alt trafic de rețea sau timere până când acel query de bază de date nu returnează un rezultat. Iată ideea cheie. Pentru a preveni blocarea loop-ului, trimiți acea muncă blocantă către un thread separat folosind run in executor. Aceasta este o altă metodă de pe event loop. Îi pasezi un thread pool executor și funcția ta sincronă de bază de date. Loop-ul predă funcția unui background thread din pool și returnează imediat un obiect awaitable. Faci await pe acel obiect. În timp ce query-ul tău de bază de date rulează în background thread, event loop-ul tău este complet liber să pună pe pauză acel task specific și să meargă să gestioneze sute de alte web request-uri. Odată ce driverul PostgreSQL returnează în sfârșit datele, thread pool-ul transmite în siguranță rezultatul înapoi către event loop. Awaitable-ul tău primește resolve, iar funcția ta async originală își reia execuția exact de unde a rămas, având acum rezultatele din baza de date. Ai două punți unidirecționale. Folosește call soon threadsafe pentru a trimite evenimente dintr-un worker thread în loop-ul tău async. Folosește run in executor pentru a scoate munca sincronă blocantă din loop-ul tău async și a o trimite într-un worker thread. Nu lăsa niciodată un apel sincron să-ți deturneze event loop-ul și nu lăsa niciodată un background thread să-ți atingă direct obiectele async. Asta e tot pentru acest episod. Mersi că ai ascultat și continuă să construiești!
18

Async Generators și Curățarea

3m 48s

Evită scurgerile de resurse cu async generators. Explorăm de ce iterația 'async for' poate lăsa conexiuni suspendate atunci când este întreruptă și cum aclosing() oferă siguranță.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 18 din 20. Iei un timeout când faci fetch la rânduri dintr-o bază de date. Codul tău gestionează excepția și merge mai departe, dar câteva zile mai târziu aplicația ta dă crash pentru că acel connection pool este complet epuizat. Ai ieșit dintr-un async loop mai devreme, iar asta a lăsat în tăcere conexiunile la baza de date deschise în background. Soluția constă în stăpânirea conceptelor de Async Generators și cleanup. Când scrii un async generator pentru a da yield la elemente în timp, adesea gestionezi resurse. Gândește-te la un database cursor. Scrii un generator care obține o conexiune, dă yield la rânduri unul câte unul și folosește un bloc try-finally pentru a returna acea conexiune în pool atunci când fetch-ul este gata. Dacă iterezi prin absolut fiecare rând, generatorul se termină, ajunge la blocul finally și face cleanup. Totul funcționează. Pericolul apare atunci când nu consumi tot generatorul. Dacă iterația ta este învelită într-un timeout, sau dacă pur și simplu dai de un break statement după ce ai găsit rândul de care ai nevoie, generatorul se pune pe pauză. Rămâne suspendat la ultimul yield. Nu a ajuns la blocul finally. Conexiunea ta la baza de date este ținută în continuare deschisă. Te-ai putea aștepta ca garbage collector-ul din Python să rezolve asta până la urmă. În cod sincron, când un generator pierde toate referințele și este garbage collected, Python injectează o excepție de exit care rulează blocurile finally. Dar codul asincron complică treaba asta. Garbage collection-ul este un proces sincron. Când garbage collector-ul găsește în cele din urmă acel async generator suspendat, nu poate rula în mod fiabil cod asincron de teardown. Event loop-ul ar putea fi ocupat, sau ar putea fi chiar închis. Să te bazezi pe garbage collector pentru a face cleanup la un async generator duce la un comportament imprevizibil și la dangling resources. Asta e partea care contează. Documentația oficială asyncio precizează explicit că nu ar trebui să te bazezi niciodată pe garbage collection pentru cleanup-ul unui async generator. Trebuie să le închizi manual. Standard library-ul oferă un tool direct pentru asta numit aclosing, care se găsește în modulul contextlib. Acesta acționează ca un async context manager. Singurul său job este să garanteze că metoda aclose a generatorului este apelată și primește await în momentul în care ai terminat cu el. În loc să dai generatorul direct într-un async for loop, îi faci wrap. Mai întâi creezi instanța generatorului. Apoi o pasezi unui statement async with aclosing. În interiorul acelui context block, rulezi acel async for loop. Când îți structurezi codul în felul ăsta, un early exit declanșează context manager-ul. Dacă un timeout întrerupe loop-ul, blocul async with prinde acel exit. Acesta face await explicit pe metoda aclose a generatorului. Asta injectează în siguranță excepția de exit în generatorul suspendat, în timp ce tu încă rulezi activ în event loop. Blocul tău finally se execută imediat, făcând await pe orice pași necesari de teardown, iar conexiunea ta la baza de date se întoarce în siguranță în pool. Ori de câte ori un async generator obține conexiuni de rețea, file descriptors sau database locks, fă-i wrap în aclosing înainte de a itera, pentru a garanta un cleanup determinist, indiferent de timeout-uri sau early breaks. Asta e tot pentru acest episod. Mersi că m-ai ascultat și keep building!
19

Stăpânirea modului Debug

3m 57s

Prinde instantaneu bug-urile de concurență. Învață cum să folosești PYTHONASYNCIODEBUG pentru a profila callback-urile lente, a descoperi coroutines fără await și a identifica excepțiile care nu au fost preluate niciodată.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 19 din 20. Serverul tău de producție se confruntă cu lag spikes misterioase, iar operațiunile tale de background înghit aleatoriu erori fără urmă. Problema nu este logica aplicației tale, ci modul în care asyncio standard ascunde greșelile de concurrency pentru a salva performanță. Stăpânirea Debug Mode-ului este răspunsul pentru a expune instantaneu aceste erori. Debug mode-ul din asyncio acționează ca un strict mode pentru event loop. By default, asyncio prioritizează viteza brută în fața verificărilor de siguranță la runtime. Asta înseamnă că atunci când lucrurile merg prost, adesea dau fail silențios. Poți activa debug mode global setând variabila de environment PYTHONASYNCIODEBUG la unu, sau rulând Python cu flag-ul dash X dev. De asemenea, îl poți activa dinamic apelând set debug true direct pe obiectul de event loop. Ia în considerare scenariul cu lag spikes. Ai un web server care gestionează mii de request-uri concurente și, dintr-o dată, un singur endpoint face ca întreaga aplicație să dea freeze. Suspectezi că o operațiune regex problematică blochează thread-ul, dar logging-ul standard îți spune doar când începe sau se termină un request, nu ce a blocat loop-ul între timp. Când debug mode este activ, event loop-ul măsoară timpul de execuție pentru fiecare callback în parte. Dacă un callback blochează loop-ul pentru mai mult de o sută de milisecunde, asyncio face log automat la un warning. Acest warning include fișierul și numărul exact al liniei unde a avut loc freeze-ul, indicându-ți direct acea căutare regex costisitoare. Acel prag de o sută de milisecunde este default, dar îl poți ajusta pentru cerințele tale specifice de latență modificând proprietatea slow callback duration de pe loop. Debug mode prinde, de asemenea, și fail-urile silențioase de execuție. O greșeală frecventă în codul async este să apelezi o funcție coroutine, dar să uiți keyword-ul await. Funcția returnează un obiect coroutine, dar logica efectivă nu rulează niciodată. În execuția normală, acel obiect este discarded în liniște. Debug mode urmărește acest lucru. Când garbage collector-ul curăță o coroutine unawaited, debug loop-ul o interceptează și emite un resource warning, arătând exact unde a fost creată acea coroutine orfană, astfel încât să poți repara invocarea. Aceeași plasă de siguranță se aplică și pentru background tasks. Dacă un task asyncio dă crash, excepția este stocată în interiorul obiectului task în sine. Dacă codul tău nu dă niciodată await explicit pe acel task sau nu îi recuperează rezultatul, excepția pur și simplu dispare. Cu debug mode activat, asyncio monitorizează ciclul de viață al fiecărui task. Dacă un task este distrus și excepția sa internă nu a fost niciodată recuperată, event loop-ul face log zgomotos la eroare, împreună cu traceback-ul care arată unde a fost generat inițial acel task. Aceste verificări adaugă overhead, așa că de obicei lași debug mode oprit în mediile normale de producție, păstrându-l pentru local development sau troubleshooting țintit. Iată ideea cheie. Activarea debug mode mută povara găsirii de bug-uri silențioase de concurrency de la propriul tău logging manual înapoi pe event loop. Dacă îți place podcastul și vrei să ne susții, caută DevStoriesEU pe Patreon. Asta e tot pentru acest episod. Mulțumesc că ai ascultat și continuă să construiești!
20

Extinderea și Custom Loops

3m 45s

Finalul. Explorăm integrarea avansată și ce presupune scrierea unui event loop personalizat sau crearea unei subclase BaseEventLoop pentru medii specializate, de înaltă performanță.

Descarcă
Salut, sunt Alex de la DEV STORIES DOT EU. asyncio, episodul 20 din 20. Ai dat de un zid de performanță cu codul tău asincron, iar profilarea indică direct către event loop-ul principal. Nu poți rescrie standard library-ul, dar ai nevoie de control low-level asupra modului exact în care sistemul gestionează socket-urile și task-urile. Răspunsul stă în extinderea și crearea de custom loops. Event loop-ul standard din asyncio nu este un black box hardcoded. Este o interfață extensibilă. A fost conceput de la zero pentru a fi complet înlocuibil de librării C de înaltă performanță sau implementări Python specializate. Majoritatea developerilor de aplicații nu vor avea niciodată nevoie să construiască un custom loop. Totuși, dacă ești autor de framework-uri sau construiești un loop optimizat precum uvloop, trebuie să ocolești comportamentul standard și să te integrezi direct cu primitivele low-level ale sistemului de operare. Pentru a construi un custom event loop, începi prin a face subclassing la BaseEventLoop. Această clasă de bază definește întregul contract pentru modul în care trebuie să se comporte operațiunile asincrone. Moștenind din ea, obții structura, dar poți face override la metode specifice pentru a intercepta și redefini operațiunile fundamentale. Gândește-te la crearea de socket-uri. Într-o aplicație standard, îi ceri lui asyncio să deschidă o conexiune, iar el folosește implementarea default de socket din Python. Dar într-o subclasă de custom loop, poți face override la metodele de creare a rețelei. Asta înseamnă că atunci când aplicația cere o conexiune de rețea, custom loop-ul tău interceptează acel call. Apoi, poți ruta acel request prin cod C extrem de optimizat, sau îl poți lega direct de feature-uri avansate de kernel pe care Python-ul standard nu le expune. Codul aplicației nu se schimbă, dar mecanismul din spate este în întregime al tău. Acest control granular se aplică și la task management. Iată ideea de bază. Event loop-ul este responsabil pentru urmărirea fiecărui task asincron. Dacă te uiți sub capota BaseEventLoop, vei găsi o metodă internă numită underscore register task. Făcând override la această metodă specifică, custom loop-ul tău interceptează un task exact în microsecunda în care este creat. De ce contează asta? Dacă construiești un custom runtime, s-ar putea să ai nevoie să urmărești diagnostice detaliate, să implementezi memory pooling specializat pentru task-uri, sau să trimiți starea task-ului direct către un serviciu de monitorizare custom. Făcând override la underscore register task, ai un hook garantat în ciclul de viață al fiecărei corutine, înainte măcar ca aceasta să înceapă execuția. Poți, de asemenea, să faci override la metoda unregister corespunzătoare, pentru a gestiona partea de cleanup exact așa cum o cere framework-ul tău. Odată ce clasa ta de custom loop este construită, trebuie să-i spui lui Python să o și folosească. Faci asta prin crearea unui custom event loop policy. Acest policy este doar un factory care dictează ce implementare de loop este creată atunci când un thread cere una. Setezi acest custom policy la nivel global. Din acel moment, oricărei funcții din standard library care cere un event loop îi va fi oferită versiunea ta optimizată și custom. Adevărata putere a asyncio nu stă doar în sintaxa async și await. Stă în faptul că întregul motor de execuție este o interfață pluggable, gata să fie înlocuită în momentul în care performanța standard îți limitează arhitectura. Pentru că asta încheie seria noastră, te încurajez să citești documentația oficială, să încerci să extinzi aceste componente hands-on, sau să vizitezi devstories dot eu pentru a sugera subiecte pentru seriile viitoare. Asta e tot pentru acest episod. Mulțumesc că m-ai ascultat și continuă să construiești!