Wróć do katalogu
Season 10 20 Odcinki 1h 19m 2026

asyncio

Wersja 3.14 — Edycja 2026. Dogłębna analiza frameworka asyncio w Pythonie, obejmująca event loop, coroutines, structured concurrency, synchronization primitives oraz zaawansowane wzorce asynchroniczne. Dla Pythona 3.14.

Python Core Programowanie asynchroniczne
asyncio
Teraz odtwarzane
Click play to start
0:00
0:00
1
Event Loop i model mentalny
Zbuduj swój podstawowy model mentalny dla asyncio. Dowiedz się, jak event loop działa niczym dyrygent orkiestry, zarządzając zadaniami w sposób kooperatywny, bez polegania na wielowątkowości (multithreading).
4m 03s
2
Coroutines a Awaitables
Obalamy mity wokół słów kluczowych async i await. Badamy kluczową różnicę między funkcją coroutine a obiektem coroutine oraz to, co faktycznie się dzieje, gdy użyjesz await na danej operacji.
3m 57s
3
Punkt wejścia asyncio.run()
Odkryj, jak bezpiecznie zainicjować aplikację asyncio. Omawiamy asyncio.run, zamykanie executorów oraz context manager Runner do obsługi złożonych cykli życia pętli.
4m 06s
4
Harmonogramowanie za pomocą Tasks
Dowiedz się, jak wykonywać operacje współbieżnie za pomocą asyncio.create_task(). Odkrywamy poważne konsekwencje działania garbage collection na zadania bez referencji.
3m 59s
5
Structured Concurrency z TaskGroups
Opanuj structured concurrency. Zrozum, jak asyncio.TaskGroup bezpiecznie zarządza wieloma współbieżnymi operacjami i zapewnia czyste zamykanie w przypadku wystąpienia wyjątków.
3m 36s
6
Anulowanie zadań i Timeouts
Poznaj mechanikę przerywania operacji. Dowiedz się, dlaczego zgłaszany jest wyjątek asyncio.CancelledError, jak go obsłużyć w bloku finally i dlaczego nigdy nie należy go ignorować.
3m 54s
7
Przekazywanie kontroli za pomocą Sleep
Zrozum prawdziwy cel asyncio.sleep(0). Odkryj, jak przekazywanie kontroli zapobiega zagłodzeniu event loopa przez pętle mocno obciążające CPU i zawieszeniu aplikacji.
3m 58s
8
Synchronizacja: Locks i Mutexes
Zapobiegaj race conditions w kodzie async. Omawiamy asyncio.Lock, dyskutujemy o jego braku bezpieczeństwa wątkowego (non-thread-safe) i pokazujemy, dlaczego locki z modułu threading zawieszą twój event loop.
4m 02s
9
Koordynowanie stanu za pomocą Events
Naucz się rozgłaszać sygnały do wielu oczekujących zadań. Wyjaśniamy, jak asyncio.Event oraz asyncio.Condition elegancko zastępują nieefektywne pętle typu polling.
3m 53s
10
Ograniczanie współbieżności za pomocą Semaphores
Chroń wrażliwe zasoby i zapobiegaj banom za przekroczenie limitów zapytań (rate-limiting). Odkryj, jak asyncio.Semaphore ogranicza współbieżne wykonywanie bez blokowania twojej architektury.
4m 31s
11
Przepływy pracy Producer-Consumer
Bezpiecznie oddziel szybkich producentów od wolnych konsumentów. Poznaj asyncio.Queue, sygnalizowanie zakończenia zadań oraz nową mechanikę zamykania dla kolejek.
3m 43s
12
Sieciowanie wysokiego poziomu za pomocą Streams
Zanurz się w wysokopoziomowe IO Streams. Omawiamy StreamReader, StreamWriter i wyjaśniamy, dlaczego pominięcie await writer.drain() może niepostrzeżenie zniszczyć pamięć twojego serwera.
3m 51s
13
Budowanie serwerów Async
Konstruuj wysoce współbieżne serwery sieciowe. Dowiedz się, jak asyncio.start_server abstrahuje połączenia klienckie, tworząc izolowane zadanie dla każdego peera.
3m 57s
14
Nieblokujące Subprocesses
Uruchamiaj polecenia powłoki asynchronicznie. Odkryj, dlaczego użycie standardowego modułu subprocess zatrzymuje event loop i jak asyncio.create_subprocess_exec to naprawia.
3m 58s
15
Futures: Niskopoziomowy most
Rozpakuj fundamenty instrukcji await. Badamy asyncio.Future, jego rolę jako ostatecznego wyniku i to, jak łączy starszy kod oparty na callbackach z nowoczesną składnią.
4m 20s
16
Transports i Protocols
Zajrzyj pod maskę, aby zobaczyć, jak asyncio komunikuje się z systemem operacyjnym. Zrozum opartą na callbackach relację 1:1 między Transports (jak poruszają się bajty) a Protocols (co oznaczają bajty).
4m 14s
17
Threading w świecie Async
Połącz światy synchroniczne i asynchroniczne. Dowiedz się, jak bezpiecznie oddelegować ciężki, blokujący kod za pomocą executorów i callbacków thread-safe bez blokowania pętli.
3m 32s
18
Async Generators i czyszczenie
Unikaj wycieków zasobów dzięki async generators. Badamy, dlaczego iteracja async for może pozostawić wiszące połączenia po przerwaniu i jak aclosing() zapewnia bezpieczeństwo.
3m 31s
19
Opanowanie trybu Debug
Błyskawicznie wyłapuj błędy współbieżności. Dowiedz się, jak używać PYTHONASYNCIODEBUG do profilowania wolnych callbacków, odkrywania coroutines bez użycia await i precyzyjnego lokalizowania nigdy nieodebranych wyjątków.
4m 05s
20
Rozszerzanie i niestandardowe Loops
Wielki finał. Omawiamy zaawansowaną integrację i to, czego potrzeba, aby napisać niestandardowy event loop lub stworzyć podklasę BaseEventLoop dla wyspecjalizowanych, wysokowydajnych środowisk.
4m 06s

Odcinki

1

Event Loop i model mentalny

4m 03s

Zbuduj swój podstawowy model mentalny dla asyncio. Dowiedz się, jak event loop działa niczym dyrygent orkiestry, zarządzając zadaniami w sposób kooperatywny, bez polegania na wielowątkowości (multithreading).

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 1 z 20. Wielu developerów słyszy słowo asynchroniczny i zakłada, że ich kod będzie wykonywał się równolegle na wielu rdzeniach procesora. Ale potem sprawdzają swoją aplikację i odkrywają, że działa ona w całości na jednym wątku. Sekretem tej wydajności bez prawdziwego paralelizmu jest event loop, a zrozumienie jego mental modelu to fundament asyncio. Event loop to centralny manager wykonywania każdej aplikacji w asyncio. Jest dokładnie tym, na co wskazuje nazwa: ciągłą pętlą, która sprawdza, czy operacje są gotowe do uruchomienia, wykonuje je, a następnie szuka kolejnej operacji. Kluczowe jest oddzielenie tego konceptu od multithreadingu. W programie opartym na multithreadingu, wykonywaniem steruje system operacyjny. OS wymusi wstrzymanie jednego wątku i przełączy się na inny, aby współdzielić czas procesora. Same wątki nie mają żadnej kontroli nad tym, kiedy zostaną wstrzymane. Wymaga to znacznego overheadu systemowego, aby zarządzać context switchami i chronić współdzieloną pamięć. Event loop działa w zupełnie innym modelu, zwanym cooperative multitasking. Wszystko działa sekwencyjnie na jednym, pojedynczym wątku. Pętla nigdy nie przerywa operacji. Zamiast tego polega na tym, że kod jawnie oddaje kontrolę do pętli, gdy musi na coś poczekać. Pomyśl o event loopie jak o pojedynczym, doświadczonym szefie kuchni w ruchliwej restauracji. Szef kuchni otrzymuje wiele zamówień jednocześnie. Jeśli stawia na kuchence duży garnek z bulionem, żeby gotował się na wolnym ogniu, nie stoi przed nim, wpatrując się w płyn, aż ten się ugotuje. Takie podejście zablokowałoby całą kuchnię i nic innego by się nie ugotowało. Zamiast tego, szef kuchni odpala palnik, zostawia garnek na wolnym ogniu i natychmiast zabiera się za krojenie warzyw do innego dania. Szef kuchni reprezentuje pojedynczy wątek wykonawczy. Event loop to ten sam szef kuchni, który nieustannie skanuje kuchnię, dokładnie wiedząc, które garnki się gotują, na których patelniach trzeba przewrócić mięso, i natychmiast przechodzi do następnego dostępnego zadania. W twoim sofcie, gotujący się garnek to zazwyczaj operacja I/O. Kiedy twój kod wysyła request do bazy danych, baza potrzebuje czasu na przetworzenie query i odesłanie danych z powrotem. Tradycyjny, synchroniczny program zawiesiłby się i czekał na response. Dzięki event loopowi, operacja rejestruje swój request, a następnie informuje pętlę, że czeka. Event loop natychmiast przełącza się na inny fragment kodu, który faktycznie ma dane gotowe do przetworzenia. Kiedy baza danych w końcu odpowie, pierwotna operacja daje znać event loopowi, że jest gotowa do wznowienia. Event loop umieszcza ją z powrotem w kolejce i wznowi jej wykonywanie, gdy tylko bieżące zadanie zrobi yield. I tu jest kluczowa sprawa. Ponieważ event loop nie może siłą zatrzymać operacji, cały system opiera się wyłącznie na kooperacji. Jeśli jedno zadanie zdecyduje się wykonać ogromne obliczenia matematyczne bez zrobienia yielda, event loop się zatrzymuje. Pojedynczy wątek jest zajęty. W naszej kuchni to tak, jakby szef kuchni postanowił ręcznie zmielić ogromny worek mąki, ignorując wszystkie inne dania. Gotujące się garnki kipią, nowe zamówienia się piętrzą, a kuchnia staje w miejscu. Event loop jest tylko tak wydajny, jak kod, który w nim działa. Prawdziwa asynchroniczna wydajność nie bierze się z wykonywania wielu obliczeń w dokładnie tym samym fizycznym momencie, ale z upewnienia się, że twój pojedynczy wątek nigdy nie marnuje ani jednej milisekundy na bezczynne czekanie na świat zewnętrzny. Jeśli chcesz pomóc nam tworzyć ten podcast, możesz nas wesprzeć, wyszukując DevStoriesEU na platformie Patreon. Dzięki za wysłuchanie, miłego kodowania wszystkim!
2

Coroutines a Awaitables

3m 57s

Obalamy mity wokół słów kluczowych async i await. Badamy kluczową różnicę między funkcją coroutine a obiektem coroutine oraz to, co faktycznie się dzieje, gdy użyjesz await na danej operacji.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 2 z 20. Piszesz funkcję, wywołujesz ją i absolutnie nic się nie dzieje. Twój kod działa bez błędów, ale baza danych jest pusta, a request sieciowy w ogóle się nie odpala. Problemem jest fundamentalne niezrozumienie tego, co tak naprawdę robi wywołanie funkcji asynchronicznej. Dzisiaj przyjrzymy się coroutines i awaitables. W zwykłym Pythonie, kiedy wywołujesz standardową funkcję, wykonuje się ona natychmiast. Funkcje asynchroniczne całkowicie łamią tę zasadę. Istnieje wyraźna różnica między coroutine function a coroutine object. Pisząc async def, tworzysz coroutine function. Kiedy wywołujesz tę funkcję w swoim kodzie, nie wykonuje ona swojego ciała. Zamiast tego, zwraca coroutine object. Pomyśl o tym jak o zamawianiu kawy. Funkcja async def to pozycja w menu. Wywołanie tej funkcji jest jak złożenie zamówienia przy kasie. Dostajesz paragon. Ten paragon to twój coroutine object. Zgłosiłeś swój zamiar, ale nie masz jeszcze swojego napoju i nikt nawet nie zaczął go robić. Aby faktycznie uruchomić proces parzenia i odebrać kawę, musisz poczekać przy ladzie. W Pythonie robisz to za pomocą słowa kluczowego await. Kiedy wpiszesz await, a po nim coroutine object, dzieją się dwie różne rzeczy. Po pierwsze, coroutine w końcu zaczyna wykonywać swój wewnętrzny kod. Po drugie, funkcja, w której umieściłeś await, całkowicie się pauzuje. Oddaje kontrolę z powrotem do Pythona, zgłaszając, że nie może ruszyć dalej, dopóki ta konkretna coroutine się nie zakończy. To pauzowanie jest główną mechaniczną różnicą w programowaniu asynchronicznym. Podczas gdy twoja funkcja jest zapauzowana i czeka na kawę, Python ma wolną rękę, żeby odpalić inny kod gdzieś indziej. To prowadzi nas do szerszego pojęcia awaitable. Awaitable to po prostu dowolny obiekt, którego Python pozwala ci użyć ze słowem kluczowym await. Wszystkie coroutines to awaitables. Kiedy widzisz await, potraktuj to jako bezpośrednie polecenie: wykonaj ten obiekt awaitable do końca i zawieś mój obecny postęp, dopóki nie zwróci on końcowego wyniku. Jeśli napiszesz funkcję asynchroniczną o nazwie fetch data, samo wywołanie fetch data zwraca coroutine object. Jeśli przypiszesz to wywołanie do zmiennej o nazwie pending request, ta zmienna przechowuje tylko niewykonaną coroutine. Sieć pozostaje całkowicie cicha. Później w twoim skrypcie, kiedy napiszesz await pending request, Python w końcu wykonuje ten request sieciowy. Wykonywanie twojego obecnego bloku kodu zatrzymuje się dokładnie w tej linijce. Kiedy serwer odpowie, wyrażenie await rozwiązuje się do zwróconych danych, a twój otaczający kod przechodzi do następnej linijki. Oto kluczowa sprawa. Słowa kluczowego await możesz użyć tylko wewnątrz funkcji async def. Ponieważ użycie await na obiekcie wymaga zapauzowania obecnego wykonywania, funkcja zawierająca await musi sama mieć możliwość bycia zapauzowaną. Dlatego właśnie asynchroniczne zachowanie propaguje się na zewnątrz. Żeby użyć await na coroutine, musisz być wewnątrz coroutine. Budujesz łańcuch zawieszonych operacji, z których wszystkie czekają na rozwiązanie taska najniższego poziomu. Pamiętaj, wywołanie funkcji asynchronicznej bez użycia await to po prostu wygenerowanie paragonu za pracę, o której wykonanie tak naprawdę nikogo nie poprosiłeś. Kod nigdy się nie wykona, dopóki nie użyjesz na nim await. Dzięki za odsłuch. Do usłyszenia następnym razem!
3

Punkt wejścia asyncio.run()

4m 06s

Odkryj, jak bezpiecznie zainicjować aplikację asyncio. Omawiamy asyncio.run, zamykanie executorów oraz context manager Runner do obsługi złożonych cykli życia pętli.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 3 z 20. Niewłaściwe użycie punktu startowego twojej asynchronicznej aplikacji może zostawić po sobie wiszące thread executory i niezamknięte async generatory. Aby uniknąć ukrytych wycieków zasobów, musisz użyć odpowiedniego narzędzia do startowania i zatrzymywania aplikacji, co prowadzi nas do entry pointu asyncio.run. Wielu programistów błędnie próbuje używać tego narzędzia do losowego odpalania pojedynczych coroutines z synchronicznego kodu. Nie do tego ono służy. Nie możesz wywołać funkcji run, gdy w dokładnie tym samym wątku działa już inny event loop asyncio. Zrobienie tego natychmiast wywołuje runtime error. Została ona zaprojektowana specjalnie po to, by być pojedynczym, wysokopoziomowym entry pointem dla programu. Wyobraź sobie inicjalizację main loopa web serwera, który koordynuje wszystkie przychodzące requesty. Masz centralną asynchroniczną funkcję, która binduje się do portu sieciowego, ustawia request handlery i utrzymuje serwer przy życiu. Przekazujesz tę pojedynczą główną funkcję do funkcji run. Kiedy to zrobisz, asyncio automatycznie zarządza całym cyklem życia event loopa. Najpierw tworzy nowy event loop i ustawia go jako aktualny, aktywny loop dla wątku. Następnie wykonuje twój główny coroutine web serwera, aż do jego zakończenia. Oto kluczowa sprawa. Najbardziej wartościowa praca, jaką wykonuje ta funkcja, dzieje się po tym, jak twój główny kod skończy się wykonywać. Przeprowadza ona rygorystyczny cleanup. Przed zwróceniem kontroli do synchronicznej części twojego programu, anuluje wszystkie pozostałe pending taski. Następnie bezpiecznie zamyka wątki w tle w domyślnym executorze. Na koniec finalizuje wszystkie async generatory przed całkowitym zamknięciem event loopa. Możesz również przekazać do tej funkcji flagę debug, która wymusza uruchomienie bazowego loopa w debug mode, aby pomóc w śledzeniu problemów z wykonaniem. Ponieważ ta standardowa funkcja na końcu robi teardown wszystkiego, tworzy to sztywną granicę. Jeśli masz scenariusz, w którym musisz odpalić kilka odrębnych asynchronicznych bloków z synchronicznego kodu, ale chcesz, żeby dzieliły ten sam event loop, wywoływanie standardowej funkcji run jedno po drugim zakończy się błędem, ponieważ za każdym razem tworzony i niszczony jest nowy loop. W takiej sytuacji używasz context managera asyncio Runner. Otwierasz blok kontekstu za pomocą standardowej instrukcji with w Pythonie. Wejście do tego bloku inicjalizuje event loop. Będąc w środku, możesz wywołać własną metodę run obiektu runner. Przekazujesz jej coroutine, ona wykonuje go do końca i zwraca wynik. Możesz wywoływać tę wewnętrzną metodę run wielokrotnie w tym samym bloku kontekstu. Event loop pozostaje przy życiu, utrzymując stan, dane w cache'u i połączenia między tymi oddzielnymi wywołaniami. Możesz skonfigurować context managera podczas jego tworzenia, przekazując flagę debug, a nawet customowy loop factory, jeśli twoje środowisko wymaga specjalistycznej implementacji event loopa. Kiedy wykonywanie w końcu opuści blok context managera, runner wykonuje dokładnie tę samą sekwencję teardown, co funkcja standalone. Czyści executory, finalizuje generatory i bezpiecznie zamyka loopa. Stabilność twojej aplikacji zależy całkowicie od tego, jak startuje i jak się kończy. Niezależnie od tego, czy używasz pojedynczego wywołania funkcji, czy context managera, kierowanie wykonywania przez te oficjalne entry pointy to jedyny sposób, aby zagwarantować, że twoje asynchroniczne zasoby przejdą niezawodny teardown po wyjściu z programu. To wszystko w tym odcinku. Dzięki za wysłuchanie i buduj dalej!
4

Harmonogramowanie za pomocą Tasks

3m 59s

Dowiedz się, jak wykonywać operacje współbieżnie za pomocą asyncio.create_task(). Odkrywamy poważne konsekwencje działania garbage collection na zadania bez referencji.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 4 z 20. Uruchamiasz proces w tle, żeby wysłać metryki systemowe. Później sprawdzasz swój dashboard i brakuje połowy danych. Nie poleciały żadne błędy. Twój kod po prostu po cichu zatrzymał się w połowie wykonywania. Dzieje się tak, ponieważ potraktowałeś swój background job jako fire-and-forget. Dzisiaj omówimy planowanie przy użyciu Tasków i dlaczego zawsze musisz trzymać referencje do rzeczy, które tworzysz. Kiedy masz coroutine, którą chcesz uruchomić współbieżnie z innym kodem, używasz funkcji create task z asyncio. Przekazujesz swoją coroutine do tej funkcji, a asyncio opakowuje ją w obiekt Task. To mówi event loopowi, żeby zaplanował taska do wykonania. Funkcja natychmiast zwraca ci nowy obiekt Task, pozwalając twojemu głównemu programowi działać dalej, podczas gdy task operuje w tle. Wielu developerów wywołuje create task i ignoruje zwracaną wartość. To ogromna pułapka. Oto kluczowa sprawa. Event loop w asyncio trzyma tylko słabe referencje do tasków, które uruchamia. Sam loop nie chroni twojego taska przed garbage collectorem w Pythonie. Jeśli nie przypiszesz zwróconego obiektu Task do zmiennej albo nie zapiszesz go w jakiejś strukturze danych, garbage collector w końcu zauważy, że nie ma do niego żadnych twardych referencji. Kiedy to nastąpi, Python niszczy obiekt Task. Nie obchodzi go, czy coroutine jest w samym środku wykonywania zapytania do bazy danych, czy czeka na odpowiedź z sieci. Task po prostu znika. Pomyśl o funkcji async o nazwie ship metrics. Formatuje ona payload z danymi i wysyła request HTTP do zewnętrznego serwera. Wywołujesz create task i przekazujesz ship metrics, ale nie przypisujesz nigdzie wyniku. Task zaczyna działać. Formatuje payload. Następnie trafia na network call i pauzuje, żeby poczekać na połączenie. Kiedy jest zapauzowany, uruchamia się garbage collector. Licznik silnych referencji wynosi zero. Task zostaje zniszczony. Serwer nigdy nie otrzymuje payloadu, a twoja aplikacja nigdy nie loguje błędu, ponieważ wykonywanie kodu po prostu przestało istnieć. Żeby temu zapobiec, musisz zawsze trzymać silną referencję do tasków, które planujesz. Jeśli tworzysz pojedynczego taska, przypisz go do zmiennej. Jeśli planujesz wiele background tasków wewnątrz pętli, dodaj je do standardowego pythonowego seta albo listy. Dopóki ten set istnieje w pamięci, silne referencje istnieją, a garbage collector zostawi twoje działające taski w spokoju. Możesz wtedy użyć callbacka, żeby usunąć taska ze swojego seta, kiedy już się zakończy. Funkcja create task przyjmuje też kilka opcjonalnych argumentów. Możesz przekazać stringa do parametru name, co przypisuje konkretny identyfikator do taska. Jest to bardzo polecane do debugowania, ponieważ znacznie ułatwia wyśledzenie, która konkretnie operacja się nie powiodła, jeśli później poleci wyjątek. Możesz też przekazać argument context, żeby ustalić konkretny stan zmiennej kontekstowej dla taska. Traktowanie operacji w tle jako fire-and-forget w końcu zemści się na tobie cichymi błędami. Jeśli prosisz event loopa o uruchomienie czegoś, musisz trzymać twardą referencję do wynikowego obiektu, aż praca zostanie całkowicie zakończona. Dzięki za wysłuchanie, miłego kodowania wszystkim!
5

Structured Concurrency z TaskGroups

3m 36s

Opanuj structured concurrency. Zrozum, jak asyncio.TaskGroup bezpiecznie zarządza wieloma współbieżnymi operacjami i zapewnia czyste zamykanie w przypadku wystąpienia wyjątków.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 5 z 20. Przed Pythonem 3.11 odpalanie wielu współbieżnych tasków było łatwe, ale bezpieczna obsługa w przypadku awarii jednego z nich była niezwykle trudna. Często kończyłeś z osieroconymi taskami w tle, które po cichu marnowały zasoby. Rozwiązaniem tego bałaganu jest ustrukturyzowana współbieżność z użyciem TaskGroup. TaskGroup to asynchroniczny context manager. Ludzie czasami mylą go ze standardową listą tasków, ale jest on znacznie bardziej rygorystyczny. Zapewnia silne gwarancje bezpieczeństwa dotyczące sposobu rozpoczynania i kończenia tasków. Wymusza regułę, że procedura nadrzędna nie może się zakończyć, dopóki wszystkie jej operacje podrzędne nie zostaną zakończone lub poprawnie anulowane. Używasz go, otwierając blok async with. Wewnątrz tego bloku wywołujesz metodę create task bezpośrednio na obiekcie grupy, aby rozpocząć swoje współbieżne operacje. Nie wrzucasz tych tasków do standardowej tablicy i nie robisz na nich await ręcznie. Zamiast tego, gdy kod dociera do końca bloku async with, TaskGroup niejawnie pauzuje. Czeka w tym miejscu, aż każdy odpalony task się zakończy. Blok po prostu nie wyjdzie przedwcześnie. Oto kluczowa sprawa. Prawdziwa siła TaskGroup tkwi w tym, jak radzi sobie z błędami. W przypadku starszych narzędzi, takich jak gather, jeśli odpaliłeś kilka tasków i jeden rzucił błędem, pozostałe nadal działały w tle. Musiałeś napisać złożoną logikę obsługi błędów, aby namierzyć ocalałe taski i je ubić. TaskGroup ogarnia to automatycznie. Weźmy scenariusz, w którym web scraper pobiera jednocześnie trzy różne endpointy API. Potrzebujesz danych użytkownika, ostatnich postów i alertów systemowych. Otwierasz TaskGroup i odpalasz trzy taski. Wszystkie zaczynają działać współbieżnie w sieci. W połowie operacji endpoint ostatnich postów łapie timeout i rzuca błędem połączenia. TaskGroup natychmiast wykrywa tę awarię. Przechwytuje błąd i automatycznie wysyła sygnał anulowania do taska z danymi użytkownika oraz taska z alertami systemowymi. Czyści te oczekujące operacje, aby nie zjadały dalej przepustowości sieci ani pamięci. Pozostałe taski rzucają wewnętrznie błędem anulowania, potwierdzając zamknięcie. Gdy wszystkie pozostałe taski zostaną bezpiecznie zatrzymane, TaskGroup pakuje pierwotny błąd połączenia w nową strukturę zwaną ExceptionGroup i rzuca nim na zewnątrz bloku kontekstu. To zachowanie sprawia, że twój asynchroniczny kod jest całkowicie przewidywalny. Jeśli wykonanie pomyślnie przejdzie przez blok, masz pewność, że każdy pojedynczy task zakończył się sukcesem. Jeśli blok rzuci ExceptionGroup, wiesz, że błąd został przechwycony, a cała reszta została prawidłowo zamknięta. Nigdy nie zostawiasz dzikich tasków działających w tle. Jeśli potrzebujesz wyników udanych tasków, możesz je wyciągnąć bezpośrednio z obiektów tasków, które utworzyłeś, pod warunkiem, że zakończyły się przed wystąpieniem błędu. Wiążąc taski ze ścisłym blokiem cyklu życia, TaskGroup gwarantuje, że współbieżne operacje wchodzą i wychodzą z twojej aplikacji jako pojedyncza, skoordynowana jednostka. To wszystko na dziś. Dzięki za wysłuchanie — idź zbudować coś fajnego.
6

Anulowanie zadań i Timeouts

3m 54s

Poznaj mechanikę przerywania operacji. Dowiedz się, dlaczego zgłaszany jest wyjątek asyncio.CancelledError, jak go obsłużyć w bloku finally i dlaczego nigdy nie należy go ignorować.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 6 z 20. Napisałeś solidny error handler, który wyłapuje wszystkie ogólne wyjątki w twoim async workerze. Ale teraz, podczas shutdownu, twój event loop zapycha się taskami zombie, które nie chcą umrzeć. Twoja siatka bezpieczeństwa tak naprawdę trzyma je przy życiu. Właśnie tym zajmiemy się dzisiaj: Task Cancellation i Timeouts. Kiedy musisz zatrzymać działający task, wywołujesz jego metodę cancel. Nie kończy to taska natychmiast, tak jak zabicie procesu systemowego. Zamiast tego, asyncio prosi o zatrzymanie, wstrzykując do taska błąd, a konkretnie asyncio CancelledError. Ten błąd jest rzucany dokładnie w obecnym lub następnym punkcie await tego taska. Coroutine zwija wtedy swój stos dokładnie tak samo, jak przy każdym standardowym błędzie w Pythonie. Ten mechanizm to również fundament dla timeoutów. Kiedy opakujesz task w funkcję timeout i timer wygaśnie, event loop nie zatrzymuje taska w magiczny sposób. Po prostu wywołuje cancel na tym tasku. Task otrzymuje CancelledError przy następnym await, zwija swój stan i ostatecznie informuje wrapper timeout, że się zatrzymał. Dopiero wtedy wrapper timeout rzuca do ciebie TimeoutError. Oto kluczowa sprawa. Od Pythona 3.8 CancelledError dziedziczy bezpośrednio po BaseException, a nie po standardowej klasie Exception. Taka decyzja projektowa zapobiega pewnemu konkretnemu, katastrofalnemu błędowi. Developerzy rutynowo owijają operacje sieciowe lub plikowe w bloki try i except, które łapią ogólne klasy Exception, żeby zapobiec crashom. Gdyby CancelledError był standardowym Exception, te bloki łapałyby sygnał cancel. Task prawdopodobnie zalogowałby warning, połknął sygnał i działał dalej jako zombie. Przenosząc CancelledError w górę hierarchii do BaseException, Python gwarantuje, że twoje codzienne error handlery nie przechwycą przypadkowo żądania cancel. Jak więc bezpiecznie zarządzać stanem, gdy task jest anulowany? Polegasz na strukturze try i finally. Wyobraź sobie serwer webowy przetwarzający przychodzący HTTP request. Użytkownik prosi o ogromny raport, ale potem zamyka okno przeglądarki. Serwer wykrywa rozłączenie i robi cancel na tasku tego requestu. Wewnątrz swojego kodu akurat robisz await na długo trwającym zapytaniu do bazy danych. Ten await nagle rzuca CancelledError. Ponieważ umieściłeś interakcję z bazą danych w bloku try, wykonanie natychmiast skacze do twojego bloku finally. Używasz tego bloku finally, żeby czysto zrobić rollback trwającej transakcji i zwrócić połączenie z bazą danych do puli. Kiedy blok finally się kończy, CancelledError bąbelkuje dalej w górę, pomyślnie kończąc task. Czasami blok finally to za mało. Jeśli absolutnie musisz wykonać asynchroniczny cleanup, na przykład wysłać network request do zdalnego mikroserwisu, żeby ogłosić przerwanie, możesz jawnie złapać CancelledError. Ale jeśli to zrobisz, musisz jawnie ponownie rzucić dokładnie ten sam błąd na końcu swojego bloku except. Jeśli go nie rzucisz ponownie, zepsujesz wewnętrzną mechanikę asyncio. Task będzie wyglądał, jakby zakończył się sukcesem, a nie został anulowany, co psuje stan twojej aplikacji i łamie structured concurrency. Zasada, o której musisz pamiętać, jest taka, że cancel to kooperacyjne żądanie, a nie siłowa komenda kill, i opiera się całkowicie na wyjątkach bąbelkujących w górę bez żadnych ingerencji. Jeśli chcesz wesprzeć podcast, możesz wyszukać DevStoriesEU na Patreonie. To wszystko w tym odcinku. Dzięki za wysłuchanie i koduj dalej!
7

Przekazywanie kontroli za pomocą Sleep

3m 58s

Zrozum prawdziwy cel asyncio.sleep(0). Odkryj, jak przekazywanie kontroli zapobiega zagłodzeniu event loopa przez pętle mocno obciążające CPU i zawieszeniu aplikacji.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 7 z 20. Czasami sekretem utrzymania responsywności serwera sieciowego jest uśpienie twoich najcięższych tasków na dokładnie zero sekund. Jeśli funkcja nigdy nie pauzuje, cała twoja aplikacja przestaje nasłuchiwać świata zewnętrznego. Żeby to naprawić, używasz yieldowania kontroli przy pomocy sleep. We frameworku asyncio, event loop uruchamia dokładnie jeden task naraz. Opiera się on całkowicie na kooperacyjnej wielozadaniowości. Task działa nieprzerwanie, dopóki nie trafi na słowo kluczowe await, które działa jak checkpoint, oddając kontrolę nad wykonaniem z powrotem do event loopa. Jeśli napiszesz funkcję async, która zawiera operację czysto CPU-bound, tworzysz wąskie gardło. Pomyśl o parsowaniu ogromnego payloadu JSON albo transformowaniu tysięcy stringów. W standardowej pętli przetwarzania danych nie ma naturalnych punktów await. Ponieważ task nigdy nie robi yield, event loop pozostaje zablokowany. Wszelkie przychodzące requesty sieciowe, odpowiedzi z bazy danych czy health checki po prostu siedzą w kolejce, zagłodzone, czekając aż twoja pętla się skończy. Natywnym sposobem na rozwiązanie tego problemu jest ręczne oddanie kontroli z powrotem do event loopa. Robisz to za pomocą konkretnego idiomu: wywołując await na asyncio dot sleep z argumentem zero. Na pierwszy rzut oka, sleep na zero sekund wygląda jak bezużyteczna operacja. Po co prosić system, żeby w ogóle nie czekał? Oto kluczowa sprawa. W sleepie na zero sekund wcale nie chodzi o upływ czasu. To jawny sygnał dla event loopa. Kiedy robisz await na sleep zero, obecna coroutine zostaje natychmiast zawieszona. Event loop przejmuje kontrolę, umieszcza twój zawieszony task na końcu kolejki runnable i sprawdza, czy jakieś inne zaplanowane taski są gotowe do wykonania. Jeśli działający w tle network handler czeka na potwierdzenie przychodzącego połączenia, dostaje swoją kolej. Kiedy inne taski trafią na swoje własne punkty await lub się zakończą, twój oryginalny task wraca na początek kolejki i wznawia działanie dokładnie tam, gdzie skończył. Zastosujmy to do konkretnego scenariusza. Piszesz funkcję async do przetwarzania milionów rekordów z pliku JSON. Jeśli puścisz pętlę while bez żadnych przerw, twój serwer będzie wyglądał na martwy. Zamiast tego wprowadzasz zmienną licznika. Wewnątrz pętli przetwarzasz rekord i inkrementujesz licznik. Następnie dodajesz prosty warunek. Jeśli licznik wskazuje, że minęło sto iteracji, robisz await asyncio dot sleep zero. To dzieli te ogromne obliczenia na łatwe do opanowania chunki. Pętla przetwarza sto rekordów, ustępuje miejsca, żeby pozwolić serwerowi odpowiedzieć na pingi albo przyjąć nowe dane, a następnie wznawia parsowanie kolejnej setki. Liczba iteracji między yieldami to parametr, który musisz dostroić. Robienie yield w każdej pojedynczej iteracji dodaje zbyt duży overhead, ponieważ zawieszanie i wznawianie coroutine ma swój mały koszt obliczeniowy. Robienie yield co dziesięć tysięcy iteracji może nadal blokować event loop na zbyt długo. Sto to rozsądny punkt wyjścia, żeby dać pętli oddychać. Wymuszenie sleepa na zero sekund to najprostszy sposób, żeby twoja aplikacja pozostała kooperacyjna, co gwarantuje, że pojedyncza, ciężka pętla nigdy nie zagłodzi reszty twojego systemu. Dzięki za wysłuchanie, miłego kodowania wszystkim!
8

Synchronizacja: Locks i Mutexes

4m 02s

Zapobiegaj race conditions w kodzie async. Omawiamy asyncio.Lock, dyskutujemy o jego braku bezpieczeństwa wątkowego (non-thread-safe) i pokazujemy, dlaczego locki z modułu threading zawieszą twój event loop.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 8 z 20. Wrzucasz standardowy lock z modułu threading do swojej aplikacji async, żeby chronić współdzielony zasób, i nagle cały twój event loop całkowicie się zawiesza. Lock zrobił swoje, ale zatrzymał wszystko inne w procesie. Żeby rozwiązać ten problem bez blokowania event loopa, używamy synchronizacji: locków i mutexów. Lock w asyncio, często nazywany mutexem, gwarantuje wyłączny dostęp do współdzielonego zasobu między taskami async. Najpierw musimy wyjaśnić częste nieporozumienie. Nie możesz użyć standardowego locka z pythonowego modułu threading wewnątrz aplikacji async. Lock z threading działa na poziomie systemu operacyjnego. Jeśli nie może uzyskać locka, wstrzymuje cały thread. Ponieważ asyncio uruchamia wiele tasków kooperatywnie w jednym threadzie, zablokowanie tego threada oznacza, że event loop się zatrzymuje. Żadne requesty sieciowe nie wychodzą, żadne timery nie tykają. Wszystko się zamraża. Lock w asyncio rozwiązuje ten problem, będąc task-safe, a nie thread-safe. Kiedy task asyncio próbuje uzyskać zablokowany mutex, nie blokuje threada. Zamiast tego zawiesza sam siebie i oddaje kontrolę z powrotem do event loopa. Pozwala to innym, niezwiązanym taskom na kontynuowanie pracy, podczas gdy pierwszy task czeka w kolejce. Przełóżmy to na konkretny scenariusz. Masz aplikację z dziesiątkami tasków async, które robią zewnętrzne wywołania API. Twój token OAuth wygasa. Dwa różne taski zauważają wygasły token dokładnie w tej samej milisekundzie. Bez synchronizacji, oba taski niezależnie wyślą request do serwera uwierzytelniającego, żeby odświeżyć token. Ta nadmiarowa praca może wyzwolić rate limity albo natychmiast unieważnić pierwszy token z powodu restrykcyjnych polityk rotacji. Żeby zapobiec temu race condition, tworzysz pojedynczy lock asyncio podczas inicjalizacji swojej aplikacji. Ten obiekt locka jest przekazywany lub współdzielony między wszystkimi twoimi taskami API. Teraz spójrz na ten flow. Task A i Task B wykrywają wygasły token. Task A jako pierwszy dociera do bloku synchronizacji i robi await na locku. Z sukcesem go uzyskuje. Task B pojawia się ułamek sekundy później i robi await na tym samym locku. Ponieważ Task A go trzyma, Task B idzie spać, pozwalając event loopowi zająć się innymi rzeczami. Kiedy wiele tasków czeka na ten sam lock, asyncio ustawia je w kolejce. Kiedy lock zostanie zwolniony, event loop budzi pierwszy task w kolejce. Task A bezpiecznie prosi o nowy token, aktualizuje współdzieloną zmienną z tokenem i zwalnia locka. W tym momencie event loop budzi Task B. Task B w końcu uzyskuje locka. Jednak przed wykonaniem requestu sieciowego, Task B ponownie sprawdza token. Widzi, że token jest już ważny, pomija krok odświeżania, zwalnia locka i kontynuuje swój główny request do API. Najbezpieczniejszym sposobem na zaimplementowanie tej logiki jest użycie asynchronicznego context managera. W swoim kodzie piszesz instrukcję async with, a po niej obiekt locka. Kiedy wykonanie wchodzi do tego bloku, czeka na wyłączny dostęp. Kiedy wykonanie wychodzi z bloku, czy to normalnie, czy z powodu błędu, który wywalił taska, automatycznie zwalnia locka. Nie musisz ręcznie wywoływać metod acquire ani release, co eliminuje ryzyko przypadkowego zostawienia zablokowanego locka na zawsze. Oto kluczowa sprawa. Lock w asyncio nie chroni twojego stanu przed innymi threadami systemu operacyjnego; chroni twój stan przed twoimi własnymi współbieżnymi taskami, które wchodzą sobie w drogę podczas robienia await na innych operacjach. Dzięki za spędzony czas. Mam nadzieję, że dowiedziałeś się czegoś nowego.
9

Koordynowanie stanu za pomocą Events

3m 53s

Naucz się rozgłaszać sygnały do wielu oczekujących zadań. Wyjaśniamy, jak asyncio.Event oraz asyncio.Condition elegancko zastępują nieefektywne pętle typu polling.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 9 z 20. Masz pięćdziesiąt tasków czekających na połączenie z bazą danych. Zdecydowanie nie chcesz, żeby robiły polling w pętli, marnując cykle CPU na sprawdzanie, czy połączenie jest gotowe. Potrzebujesz pojedynczego sygnału broadcastowego, który powie im wszystkim, żeby zaczęły robić zapytania dokładnie w tym samym momencie. Właśnie tym zajmuje się koordynacja stanu za pomocą obiektów Event i Condition. Event w asyncio zarządza prostą, wewnętrzną flagą boolean. Na początku ma wartość false. Zanim przyjrzymy się temu flow, wyjaśnijmy częste nieporozumienia między Eventami a Lockami. Lock daje wyłączny dostęp dokładnie jednemu taskowi naraz, blokując pozostałe. Event robi coś dokładnie odwrotnego. Powiadamia wiele czekających tasków jednocześnie, pozwalając im wszystkim ruszyć w tym samym czasie. Pomyśl o tym scenariuszu z połączeniem do bazy danych. Twój background task próbuje nawiązać połączenie. W międzyczasie twoje pięćdziesiąt workerów dociera do punktu, w którym potrzebują bazy. Każdy worker wywołuje metodę wait na współdzielonym obiekcie Event. Ponieważ wewnętrzna flaga to false, wszystkie pięćdziesiąt tasków zostaje zawieszonych. Siedzą bezczynnie. W końcu background task kończy się sukcesem i wywołuje metodę set na obiekcie Event. Flaga zmienia się na true. Natychmiast wszystkie pięćdziesiąt zawieszonych workerów budzi się i wznawia działanie. Jeśli później będziesz musiał zamknąć połączenie, możesz wywołać metodę clear na obiekcie Event. Flaga wraca do wartości false, a wszystkie przyszłe wywołania wait znów będą blokować. Możesz też w dowolnym momencie sprawdzić aktualny status flagi, wywołując metodę is_set, która zwraca true lub false bez blokowania taska. To by było na tyle, jeśli chodzi o proste sygnały broadcastowe. Czasami pojedyncza flaga boolean nie wystarcza. Możesz mieć wiele tasków, które muszą poczekać, aż współdzielony zasób osiągnie określony, złożony stan, i potrzebują wyłącznego dostępu, żeby bezpiecznie ten stan sprawdzić lub zmodyfikować. I tutaj wkracza asyncio Condition. Condition opiera się na wewnętrznym Locku. Żeby zrobić cokolwiek z obiektem Condition, task musi najpierw zrobić na nim acquire. Po jego uzyskaniu, task sprawdza współdzielony stan. Jeśli stan nie jest taki, jakiego potrzebuje task, wywołuje on metodę wait na obiekcie Condition. I tu jest kluczowa sprawa. Wywołanie wait na obiekcie Condition robi dwie rzeczy naraz: zwalnia wewnętrznego Locka, pozwalając innym taskom na dostęp do stanu, i zawiesza obecnego taska. Kiedy ten task jest zawieszony, inny task może zrobić acquire na Condition, zmienić współdzielony stan, a następnie wywołać metodę notify. Metoda notify przyjmuje argument określający dokładnie, ile czekających tasków należy obudzić, domyślnie jeden. Możesz też wywołać notify_all, żeby obudzić wszystkie naraz. Kiedy zawieszony task się budzi, nie rusza od razu. Musi poczekać na ponowne uzyskanie wewnętrznego Locka, zanim metoda wait zwróci wynik. Ponieważ inny task może przejąć Locka i zmienić stan, zanim obudzony task doczeka się swojej kolei, wywołanie wait jest prawie zawsze umieszczane w pętli while, która ciągle sprawdza pożądany stan. Kiedy odzyska Locka, a stan jest poprawny, może bezpiecznie ruszyć dalej i ostatecznie zwolnić Condition. Wybierając między nimi, pamiętaj, że Event to prosty broadcast mówiący taskom, że wydarzyła się jednorazowa akcja, podczas gdy Condition pozwala taskom bezpiecznie czekać na złożoną zmianę stanu bez ciągłego robienia pollingu na zablokowanym zasobie. Dzięki za spędzenie ze mną tych kilku minut. Do następnego razu, trzymaj się.
10

Ograniczanie współbieżności za pomocą Semaphores

4m 31s

Chroń wrażliwe zasoby i zapobiegaj banom za przekroczenie limitów zapytań (rate-limiting). Odkryj, jak asyncio.Semaphore ogranicza współbieżne wykonywanie bez blokowania twojej architektury.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 10 z 20. Wysłanie dziesięciu tysięcy asynchronicznych requestów do wrażliwego zewnętrznego API to bardzo skuteczny sposób na permanentnego bana na twój adres IP. Twój kod działa bez zarzutu, ale serwer po drugiej stronie pada pod wpływem nagłego skoku ruchu. Aby chronić zewnętrzne serwisy i swój własny dostęp, musisz throttlować swoją aplikację. Tą tarczą jest ograniczanie concurrency za pomocą semaforów. Warto od razu wyjaśnić pewne powszechne nieporozumienie. Semafor to nie rate limiter. Nie nakłada limitu na to, ile requestów twój program wysyła w ciągu sekundy. Zamiast tego ogranicza concurrent operations. Ściśle kontroluje, ile tasków może wykonywać konkretny blok operacji sieciowych lub plikowych w dokładnie tym samym momencie. Jeśli task zakończy swój API call w dziesięć milisekund, ten slot natychmiast zwalnia się dla następnego taska w kolejce. Nadal możesz przetwarzać setki operacji na sekundę, pod warunkiem, że w locie nie znajduje się jednocześnie więcej operacji, niż pozwala na to limit. Semaphore z asyncio zarządza prostym wewnętrznym licznikiem. Kiedy tworzysz obiekt semafora, podajesz mu wartość początkową. Weźmy scenariusz ograniczania wychodzących HTTP requestów do delikatnego zewnętrznego API do dokładnie dziesięciu concurrent connections. Inicjalizujesz swój semafor z wartością dziesięć. Zanim jakikolwiek asynchroniczny task wykona network request, musi zrobić acquire na semaforze. Ta akcja zmniejsza wewnętrzny licznik o jeden. Kiedy network request się kończy, task robi release semafora, zwiększając licznik z powrotem o jeden. I tu jest kluczowa sprawa. Jeśli dziesięć tasków zrobiło już acquire na semaforze, licznik wynosi zero. Kiedy jedenasty task próbuje zrobić acquire, ten task zostaje zawieszony. Metoda acquire blokuje postęp, dopóki jeden z pierwszych dziesięciu tasków nie skończy i nie zrobi release. Ten prosty numeryczny lock gwarantuje, że nigdy nie przekroczysz swojego twardego limitu dziesięciu aktywnych połączeń. W praktyce rzadko powinieneś wywoływać metody acquire i release ręcznie. Zamiast tego używasz semafora jako asynchronicznego context managera. Opakowując swój HTTP request w asynchroniczny statement with, Python gwarantuje, że na semaforze zostanie wywołany release, kiedy wyjdziesz z bloku kodu. Ten release następuje nawet jeśli API rzuci timeoutem, zrzuci połączenie lub wyrzuci unhandled exception. Jeśli próbujesz robić manualne release'y, a błąd pominie twoje wywołanie release, ten concurrency slot przepada na zawsze. Jeśli stracisz wszystkie dziesięć slotów przez przejściowe błędy sieciowe, cały twój program po cichu wpadnie w deadlock. Ze standardowym semaforem wiąże się pewne subtelne niebezpieczeństwo. Jeśli błąd logiczny w twoim kodzie sprawi, że task zrobi release semafora więcej razy niż zrobił acquire, wewnętrzny licznik wzrośnie powyżej twojego pierwotnego limitu dziesięciu. Nagle twoja tarcza concurrency pęka, a ty nieświadomie wysyłasz dwanaście lub piętnaście jednoczesnych requestów. Aby temu zapobiec, powinieneś użyć Bounded Semaphore z asyncio. Bounded Semaphore zachowuje się dokładnie tak samo jak standardowy semafor, ale śledzi początkową wartość, którą mu podałeś. Jeśli jakiś zbuntowany task spróbuje zrobić release semafora powyżej tego początkowego limitu, Bounded Semaphore natychmiast rzuci ValueError. Wysypuje się wcześnie i głośno, zamiast po cichu przeciążać zewnętrzne API. Zawsze domyślnie używaj Bounded Semaphore, chyba że masz bardzo konkretny powód architektoniczny, aby dynamicznie pompować swoje limity concurrency. Bounded Semaphores wyłapują logiczne błędy z release w momencie ich wystąpienia, utrzymując twoje limity połączeń do API w ryzach i zapewniając przewidywalne działanie twoich systemów. To wszystko w tym odcinku. Dzięki za wysłuchanie i twórz dalej!
11

Przepływy pracy Producer-Consumer

3m 43s

Bezpiecznie oddziel szybkich producentów od wolnych konsumentów. Poznaj asyncio.Queue, sygnalizowanie zakończenia zadań oraz nową mechanikę zamykania dla kolejek.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 11 z 20. Masz asynchroniczny web server obsługujący tysiące requestów na sekundę i dla każdego requestu musisz zapisać log na dysku. Jeśli twój serwer czeka na zakończenie zapisu na dysku przed zwróceniem odpowiedzi, wydajność drastycznie spada. Najbardziej niezawodny sposób na odseparowanie szybkich producentów od wolnych konsumentów w asynchronicznym Pythonie jest wbudowany prosto w bibliotekę standardową. Dzisiaj przyjrzymy się workflowom Producer-Consumer z wykorzystaniem kolejek asyncio. Niektórzy developerzy przychodzący ze świata programowania wielowątkowego zakładają, że muszą obudować tę kolejkę lockami, aby zapobiec race condition. Wcale nie musisz. Kolejka asyncio została zaprojektowana specjalnie dla współbieżnych tasków działających w pojedynczym event loopie. Jest z natury bezpieczna dla tych tasków. Zostaw thread-safe kolejki ze standardowego modułu queue dla wątków; do asynchroniczności używaj wersji z asyncio. Pomyśl o kolejce jak o rurze. Z jednej strony masz producentów, którzy wrzucają do niej elementy. Z drugiej strony masz konsumentów, którzy je wyciągają. Wróćmy do naszego scenariusza z logowaniem. Twój request handler to producent. Odbiera przychodzący request, formatuje event logu i wywołuje asynchroniczną metodę put na kolejce. Jeśli przy tworzeniu kolejki ustawisz jej maksymalny rozmiar, zyskujesz automatyczny backpressure. Gdy kolejka jest pełna, awaitowanie metody put pauzuje producenta, dopóki nie zwolni się miejsce. To zapobiega wyczerpaniu pamięci systemowej przez nagły skok ruchu. Po drugiej stronie rury masz osobny background task, który działa jako konsument. Ten task działa w nieskończonej pętli. Wywołuje asynchroniczną metodę get na kolejce. Jeśli kolejka jest pusta, konsument bezpiecznie idzie spać. Event loop wybudza go dokładnie w momencie, gdy producent wrzuci nowy event logu do rury. Konsument pobiera event, zapisuje go na dysku, a następnie sygnalizuje, że konkretna praca została zakończona, wywołując metodę o nazwie task done. Zarządzanie tym przepływem podczas teardownu aplikacji jest kluczowe. Jeśli musisz zrobić graceful shutdown swojego web servera, chcesz mieć pewność, że wszystkie zakolejkowane eventy logów faktycznie zapiszą się na dysku. Kolejka ma metodę o nazwie join. Kiedy zrobisz await na join, twój program blokuje się, dopóki liczba wywołań task done nie zrówna się dokładnie z liczbą elementów pierwotnie wrzuconych do kolejki. To gwarantuje, że każdy pojedynczy kawałek pracy został w pełni przetworzony. I tu jest kluczowa sprawa. Python 3.13 wprowadził nową metodę na kolejce o nazwie shutdown. Wcześniej czyste zatrzymanie pętli producent-konsument wymagało przekazywania specjalnych wartości typu sentinel, na przykład wrzucenia obiektu None do kolejki, tylko po to, by powiedzieć konsumentowi, żeby wyszedł z pętli. Teraz możesz po prostu wywołać shutdown. Kiedy to zrobisz, każdy task, który jest obecnie zablokowany w oczekiwaniu na put lub get, natychmiast obrywa wyjątkiem QueueShutDown. Łapiesz ten wyjątek w swoich worker taskach, sprzątasz zasoby i wychodzisz czysto, bez żadnej kruchej logiki opartej na sentinelach. Projektując system w asyncio, pamiętaj, że kolejki to nie tylko struktury danych; to mechanizmy kontroli przepływu, które natywnie obsługują backpressure, utrzymując twój memory footprint na stabilnym poziomie, nawet gdy producenci mocno wyprzedzają konsumentów. To wszystko w tym odcinku. Dzięki za wysłuchanie i kodujcie dalej!
12

Sieciowanie wysokiego poziomu za pomocą Streams

3m 51s

Zanurz się w wysokopoziomowe IO Streams. Omawiamy StreamReader, StreamWriter i wyjaśniamy, dlaczego pominięcie await writer.drain() może niepostrzeżenie zniszczyć pamięć twojego serwera.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 12 z 20. Wysyłasz dane przez połączenie sieciowe, a twój loop wygląda idealnie. Ale za kulisami twoja aplikacja po cichu zjada gigabajty pamięci, dopóki system jej nie ubije. Problem zazwyczaj sprowadza się do braku jednej linijki kodu, która obsługuje flow control. Dlatego dzisiaj przyjrzymy się high-level networkingowi z wykorzystaniem streamów. Asyncio zapewnia high-level API do pracy z połączeniami sieciowymi bez dotykania raw sockets czy low-level transport protocols. Aby nawiązać połączenie TCP, używasz top-level funkcji o nazwie open_connection. Przekazujesz jej string z hostem i integer z portem. Natychmiast zwraca tuplę dwóch obiektów: StreamReader i StreamWriter. Jeśli budujesz serwer zamiast klienta, używasz start_server. Podajesz callback, hosta i port. Za każdym razem, gdy podłącza się nowy klient, asyncio odpala twój callback, przekazując mu dedykowany reader i writer dla tego konkretnego połączenia. StreamReader to twój interfejs do odbierania danych. Udostępnia asynchroniczne metody do wyciągania bajtów z sieci. Możesz odczytać konkretną, maksymalną liczbę bajtów za pomocą metody read. Jeśli parsujesz line-based protocols, możesz czytać aż do konkretnego separatora, takiego jak znak nowej linii, używając metody readuntil. Jeśli twój protokół wymaga nagłówka o stałym rozmiarze, możesz użyć readexactly, które poczeka, aż dotrze dokładnie taka liczba bajtów. Ponieważ wszystkie te operacje zależą od ruchu w sieci i latency, pauzują one coroutine, co oznacza, że musisz je awaitować. Drugim elementem tej układanki jest StreamWriter. Ten obiekt zajmuje się wysyłaniem danych na zewnątrz. Używasz metody write, aby wrzucić bajty do streamu. I tu jest kluczowa sprawa. Metoda write to zwykła funkcja, a nie asynchroniczna. Nie awaitujesz jej. Kiedy wywołujesz write, nie wrzucasz danych natychmiast do sieci. Po prostu odkładasz dane do wewnętrznego buffera asyncio. Pod spodem event loop próbuje sflushować ten buffer do sieci w tle. I to właśnie ten buffer jest miejscem, w którym developerzy wpadają w kłopoty. Pomyśl o kliencie TCP wysyłającym ogromny payload pliku do wolnego serwera. Jeśli wrzucisz wywołanie write w ciasny loop czytający chunki z lokalnego dysku, Python odczyta plik znacznie szybciej, niż sieć zdoła go przesłać. Ponieważ write nie blokuje twojego kodu, twój loop kręci się dalej. Wewnętrzny buffer pochłania cały plik, zjadając całą dostępną pamięć systemową. I tutaj do gry wchodzi backpressure. Aby zarządzać flow control, musisz sparować swoje wywołania write z metodą drain. Metoda drain jest asynchroniczna, co oznacza, że ją awaitujesz. Kiedy awaitujesz drain, mówisz event loopowi, żeby zapauzował twoją coroutine, jeśli wewnętrzny buffer przekroczył swój high-water mark. Twój kod czeka, aż proces w tle przepchnie wystarczająco dużo danych przez sieć, aby zmniejszyć buffer do bezpiecznego rozmiaru. Sieć ma czas na nadrobienie zaległości, buffer się opróżnia, a zużycie pamięci pozostaje na stabilnym poziomie. Kiedy skończysz wysyłać plik, wywołujesz metodę close na writerze. Podobnie jak write, close nie jest funkcją async. Aby upewnić się, że połączenie faktycznie zamyka się czysto, a wszystkie końcowe bajty są sflushowane zanim twój program pójdzie dalej, zaraz po tym awaitujesz metodę wait_closed. StreamWriter sprawia, że pisanie do sieci wydaje się natychmiastowe, ale fizyka nadal obowiązuje. Zawsze awaituj drain po wywołaniu write, aby upewnić się, że twoja aplikacja respektuje rzeczywistą prędkość połączenia sieciowego. Dzięki za wysłuchanie, happy coding wszystkim!
13

Budowanie serwerów Async

3m 57s

Konstruuj wysoce współbieżne serwery sieciowe. Dowiedz się, jak asyncio.start_server abstrahuje połączenia klienckie, tworząc izolowane zadanie dla każdego peera.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 13 z 20. Budowanie wysoce współbieżnego serwera TCP w Pythonie zazwyczaj oznacza zmaganie się z thread poolami lub złożoną konfiguracją event loopa. W rzeczywistości możesz obsłużyć tysiące połączeń w mniej niż dziesięciu linijkach kodu. Właśnie to omówimy dzisiaj, budując asynchroniczne serwery z wykorzystaniem streamów asyncio. Podstawą serwera sieciowego w asyncio jest funkcja o nazwie start_server. Przekazujesz jej trzy rzeczy: callback, adres IP i port. Kiedy zrobisz await na start_server, binduje się on do tego adresu i zaczyna nasłuchiwać przychodzących połączeń TCP na wskazanym przez ciebie interfejsie sieciowym. Programiści często zakładają, że muszą ręcznie przechwytywać te przychodzące połączenia i pisać boilerplate, żeby przekazywać je do worker threadów lub własnych background tasków. To całkowicie niepotrzebne. Framework ogarnia współbieżność za ciebie. Za każdym razem, gdy nowy klient łączy się z twoim portem, start_server automatycznie spawnuje zupełnie nowy task asyncio, dedykowany wyłącznie temu konkretnemu klientowi. Pomyśl o budowaniu prostego serwera czatu. Kiedy połączy się twój pierwszy użytkownik, start_server odpala twój callback i przekazuje mu dwa obiekty: stream reader i stream writer. Jeśli pięćdziesięciu kolejnych użytkowników połączy się jednocześnie, natychmiast odpala się pięćdziesiąt oddzielnych tasków, które uruchamiają dokładnie ten sam callback. Każdy task otrzymuje własną, odizolowaną parę readera i writera. Wewnątrz swojego callbacku piszesz logikę tak, jakbyś rozmawiał tylko z jedną osobą naraz. Używasz obiektu readera do nasłuchiwania przychodzących wiadomości. Robisz await na metodzie read readera, określając maksymalną liczbę bajtów, jaką chcesz przyjąć, na przykład sto bajtów. Reader daje ci surowe bajty z sieci, które dekodujesz do standardowego stringa. Aby odpowiedzieć klientowi, odwracasz ten proces. Kodujesz swojego stringa z odpowiedzią z powrotem na bajty i przekazujesz go bezpośrednio do obiektu writera. I tu jest kluczowa sprawa. Przekazywanie danych do writera nie jest operacją asynchroniczną, ale upewnienie się, że dane faktycznie opuszczą fizyczną maszynę, już tak. Po przekazaniu danych do writera, musisz zrobić await na metodzie drain writera. Drainowanie pauzuje twój obecny task klienta do momentu, aż bufor sieciowy systemu operacyjnego będzie miał wystarczająco dużo wolnego miejsca, aby wypchnąć bajty w sieć. Ten krok jest krytyczny, ponieważ zapobiega zużyciu całej dostępnej pamięci przez twój serwer, jeśli klient ma wolne połączenie sieciowe. Kiedy rozmowa się kończy lub jeśli klient się rozłączy, mówisz writerowi, żeby się zamknął. Następnie robisz await na metodzie wait_closed writera, aby upewnić się, że wszystkie ostatnie bajty zostały przesłane, a socket pod spodem zamyka się czysto. Wracając do twojej głównej funkcji setup, start_server zwrócił obiekt serwera. Domyślnie serwer przestaje nasłuchiwać, jeśli główny skrypt Pythona dotrze do końca swoich instrukcji. Aby utrzymać twój pokój czatu otwarty w nieskończoność, bierzesz ten obiekt serwera i robisz await na jego metodzie serve_forever. To blokuje główny task asyncio w nieskończonej pętli, po cichu akceptując nowe połączenia i spawnując nowe taski klienta w tle. Prawdziwą siłą tego designu jest to, że abstrahuje on złożoność sieciową. Piszesz prosty, sekwencyjny kod dla pojedynczego, izolowanego połączenia, a event loop automatycznie skaluje go na współbieżne taski. Jeśli chcesz pomóc wesprzeć program, możesz wyszukać DevStoriesEU na Patreonie. To wszystko w tym odcinku. Dzięki za wysłuchanie i twórz dalej!
14

Nieblokujące Subprocesses

3m 58s

Uruchamiaj polecenia powłoki asynchronicznie. Odkryj, dlaczego użycie standardowego modułu subprocess zatrzymuje event loop i jak asyncio.create_subprocess_exec to naprawia.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 14 z 20. Budujesz asynchroniczne web API, odpalasz standardowe polecenie systemowe wewnątrz endpointu i nagle wszystkie inne współbieżne taski zostają sparaliżowane. Nic się nie rusza, dopóki to polecenie systemowe się nie zakończy. Winowajcą jest standardowy moduł subprocess w Pythonie, a rozwiązanie tego problemu wymaga użycia nieblokujących subprocessów. Wywołanie funkcji takiej jak standardowe subprocess dot run wykonuje polecenie systemu operacyjnego i czeka na jego zakończenie. W asynchronicznej aplikacji w Pythonie, event loop działa na jednym wątku. Kiedy zablokujesz ten wątek czekając na system operacyjny, event loop się zatrzymuje. Każdy inny współbieżny request do twojego API jest zamrożony. Aby to naprawić, asyncio udostępnia własne funkcje do subprocessów, zaprojektowane specjalnie dla event loopa. Głównym narzędziem jest asyncio dot create subprocess exec. Oto kluczowa sprawa. Ta funkcja nie wykonuje polecenia bezpośrednio w Pythonie. Prosi system operacyjny o utworzenie child processu, ale zamiast blokować w oczekiwaniu na wynik, natychmiast oddaje kontrolę z powrotem do event loopa. Twoje API obsługuje inne requesty, podczas gdy zewnętrzny program sobie działa. Weźmy scenariusz web API, które konwertuje pliki wideo za pomocą FFmpeg. Chcesz odpalić konwersję i streamować logi wyjściowe z powrotem do użytkownika w czasie rzeczywistym. Wewnątrz swojego asynchronicznego endpointu wywołujesz create subprocess exec. Przekazujesz nazwę programu, FFmpeg, a następnie jego argumenty. Aby przechwycić logi, mówisz funkcji, żeby przekierowała standard output i standard error do pipe'ów asyncio. Funkcja zwraca obiekt Process z asyncio. Ten obiekt reprezentuje uruchomione polecenie systemu operacyjnego i daje ci asynchroniczne hooki do interakcji z nim. Ponieważ przekierowałeś wyjścia do pipe'ów, obiekt Process wystawia je jako asynchroniczne stream readery. Czytasz logi FFmpeg iterując asynchronicznie po streamie standard error, ponieważ FFmpeg zazwyczaj tam loguje. Dla każdej linijki wygenerowanej przez zewnętrzny proces, twój async loop się budzi, czyta linijkę i streamuje ją z powrotem do użytkownika. Czekając na kolejną linijkę, event loop Pythona wraca prosto do obsługi innych użytkowników. Otrzymujesz streamowanie logów w czasie rzeczywistym bez zamrażania serwera. Jeśli nie musisz streamować wyjścia linijka po linijce, obiekt Process dostarcza również asynchroniczną metodę communicate. Robisz await na communicate, aby wysłać dane do standard input i odczytać wszystkie dane ze standard output i standard error za jednym razem. To utrzymuje loopa wolnym, dopóki zewnętrzny proces całkowicie się nie zakończy i nie zwróci danych. Jeśli obsługiwałeś streamy ręcznie, tak jak w przykładzie z FFmpeg, zamiast tego robisz await na metodzie wait w obiekcie Process, aby poczekać na zakończenie procesu i pobrać jego exit code. Event loop nie przejmuje się tym, czy to system operacyjny wykonuje faktyczne obliczenia; jeśli twój kod w Pythonie czeka synchronicznie na odpowiedź od systemu operacyjnego, cała twoja asynchroniczna aplikacja jest martwa. To wszystko w tym odcinku. Dzięki za wysłuchanie i twórz dalej!
15

Futures: Niskopoziomowy most

4m 20s

Rozpakuj fundamenty instrukcji await. Badamy asyncio.Future, jego rolę jako ostatecznego wyniku i to, jak łączy starszy kod oparty na callbackach z nowoczesną składnią.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 15 z 20. Piszesz czysty, nowoczesny kod asynchroniczny, ale w końcu musisz zintegrować się ze starą, upartą biblioteką, która opiera się wyłącznie na callbackach. Nie możesz bezpośrednio użyć await na callbacku, co psuje twój cały asynchroniczny flow. Mechanizmem, który łączy te dwa światy, są Futures: niskopoziomowy pomost. Od razu wyjaśnijmy częste nieporozumienie. Ludzie często mylą Taski z Futures. Task to konkretna podklasa Future. Task opakowuje coroutine i aktywnie wrzuca ją na event loop, sterując jej wykonaniem krok po kroku. Future niczego nie uruchamia. Nie ma własnej logiki wykonania. Jest po prostu kontenerem stanu. To niskopoziomowy prymityw reprezentujący ostateczny wynik operacji asynchronicznej. Pisząc w nowoczesnym Pythonie, prawie nigdy nie tworzysz instancji Future bezpośrednio. Event loop tworzy je pod maską. Ale kiedy musisz opakować starszy kod oparty na callbackach, konstruujesz je ręcznie. Wyobraź sobie scenariusz, w którym używasz starszej biblioteki protokołów sieciowych. Ma ona metodę request, która przyjmuje adres sieciowy, success callback i failure callback. Chcesz, aby twoja nowoczesna funkcja asynchroniczna po prostu wywołała await na tym requeście. Oto jak możesz zasypać tę przepaść. Wewnątrz swojej funkcji asynchronicznej pobierasz aktualnie działający event loop i prosisz go o utworzenie nowego obiektu Future. W tym dokładnie momencie Future jest w stanie pending. Jest pusty i czeka. Następnie piszesz małą funkcję success callback. Kiedy zostanie odpalona, ta funkcja bierze przychodzące dane i wywołuje metodę set_result na twoim obiekcie Future. Piszesz również error callback, który wywołuje metodę set_exception na tym samym obiekcie Future. Przekazujesz obie te funkcje do starszej metody request i uruchamiasz wywołanie sieciowe. Na koniec robisz await na obiekcie Future. Oto kluczowa sprawa. Użycie await na obiekcie Future w stanie pending pauzuje obecną coroutine. Oddaje kontrolę z powrotem do event loopa, pozwalając innym Taskom na działanie. Twój kod jest zamrożony na tej instrukcji await. W międzyczasie starszy klient wykonuje swoje sieciowe operacje wejścia i wyjścia w tle. Kiedy dane nadejdą, starszy klient odpala twój success callback. Twój callback wywołuje set_result na obiekcie Future. Future natychmiast przechodzi ze stanu pending w stan finished. Event loop zauważa tę zmianę stanu. Budzi coroutine, która czekała na ten Future, rozpakowuje zapisany wynik, a twoja funkcja asynchroniczna wznawia działanie, tak jakby zrobiła await na natywnej coroutine. Jeśli wywołanie sieciowe się nie powiedzie, twój error callback ustawia wyjątek na obiekcie Future. Kiedy event loop budzi coroutine, rzuca dokładnie ten wyjątek w linijce z await. Future ma surowe zasady dotyczące stanu. Może wyjść ze stanu pending tylko raz. Jeśli callback spróbuje wywołać set_result na obiekcie Future, który jest już w stanie finished, Python rzuci wyjątek InvalidStateError. Możesz również ręcznie anulować Future. Jeśli to zrobisz, wejdzie on w stan cancelled, a każda coroutine, która robi na nim await, natychmiast otrzyma wyjątek asyncio CancelledError. Obiekty Future stanowią niezbędne spoiwo strukturalne między sterowanymi zdarzeniami callbackami a proceduralnie wyglądającymi instrukcjami await. Zrozumienie, że każda instrukcja await ostatecznie pauzuje wykonanie, dopóki niskopoziomowy Future nie zostanie oznaczony jako finished, daje ci pełną jasność, jak asynchroniczny Python faktycznie działa pod spodem. To wszystko w tym odcinku. Dzięki za wysłuchanie i koduj dalej!
16

Transports i Protocols

4m 14s

Zajrzyj pod maskę, aby zobaczyć, jak asyncio komunikuje się z systemem operacyjnym. Zrozum opartą na callbackach relację 1:1 między Transports (jak poruszają się bajty) a Protocols (co oznaczają bajty).

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 16 z 20. Kiedy używasz wysokopoziomowych strumieni asyncio, twój kod jest czysty, sekwencyjny i można na nim bezpiecznie używać await. Ale pod tymi przyjaznymi coroutines kryje się mocno zoptymalizowany, oparty na callbackach silnik, który radzi sobie z chaotycznymi wywołaniami systemowymi. Aby zrozumieć, jak twoja aplikacja w Pythonie faktycznie komunikuje się z siecią, musisz przyjrzeć się transportom i protokołom. Te dwie abstrakcje stanowią fundament komunikacji sieciowej w asyncio. Zawsze działają w parze. Protokół obsługuje logikę aplikacji, decydując, jakie bajty wysłać i jak interpretować przychodzące dane. Transport zajmuje się mechaniką. Nie obchodzi go, co oznaczają twoje dane ani jak są sformatowane. Jego jedynym zadaniem jest ustalenie, jak przepchnąć te bajty przez sieć. Dzisiaj skupiamy się wyłącznie na warstwie transportowej. Pomyśl, co się dzieje, gdy piszesz bezpośrednio do nieblokującego socketu TCP. Musisz zapytać system operacyjny, czy socket jest gotowy. Musisz obsłużyć częściowe zapisy, jeśli bufor sieciowy jest pełny. Musisz śledzić, które bajty faktycznie zostały wysłane, a które trzeba będzie spróbować wysłać ponownie później. Transport w asyncio ukrywa całą tę złożoność. Działa jak nieprzezroczysty wrapper wokół surowego socketu i niskopoziomowych wywołań systemowych. Zazwyczaj nigdy nie tworzysz instancji transportu samodzielnie. Zamiast tego wywołujesz metodę event loopa, aby utworzyć połączenie sieciowe. Event loop konfiguruje socket, tworzy transport, łączy go z twoim protokołem i zwraca ci tę parę. Oto kluczowa sprawa. Po nawiązaniu połączenia, transport przejmuje buforowanie wejścia i wyjścia. Kiedy twój protokół chce wysłać wiadomość, po prostu przekazuje porcję bajtów do metody write transportu. Transport nie blokuje twojego kodu w oczekiwaniu na sieć. Natychmiast wrzuca te bajty do swojego wewnętrznego bufora. Następnie transport współpracuje w tle z event loopem, odpalając nieblokujące wywołania socketu do systemu operacyjnego. Jeśli system może w tej chwili przyjąć tylko połowę bajtów, transport zatrzymuje resztę i próbuje ponownie w kolejnej iteracji event loopa. Twoja aplikacja nigdy nie musi ręcznie zarządzać tą kolejką. Flow control jest wbudowany prosto w ten mechanizm. Jeśli zapisujesz dane szybciej, niż sieć jest w stanie je wysłać, wewnętrzny bufor transportu zacznie się zapełniać. Kiedy osiągnie wyznaczony limit, transport odpala odpowiedni callback w twoim protokole, aby wstrzymać zapisywanie. Kiedy bufor w końcu się opróżni, odpala kolejny callback, aby je wznowić. Po stronie odbiorczej, transport nasłuchuje event loopa. Kiedy system operacyjny zasygnalizuje, że nadeszły nowe bajty, transport wyciąga je z socketu i przekazuje bezpośrednio do protokołu poprzez callback. Wszystko na tym niskim poziomie jest w pełni oparte na callbackach. Nie ma tu żadnych awaitables. Transporty zapewniają również standardowe metody do zarządzania cyklem życia połączenia. Możesz łagodnie zamknąć transport, co daje mu sygnał do zakończenia wysyłania zbuforowanych danych przed bezpiecznym zamknięciem socketu. Jeśli coś pójdzie nie tak, możesz wywołać metodę abort, aby natychmiast zerwać połączenie, odrzucając wszystko, co zostało w kolejce. A jeśli twój protokół musi wiedzieć, z kim rozmawia, transport udostępnia metodę do żądania dodatkowych informacji. Pozwala to zajrzeć pod warstwę abstrakcji i pobrać adres IP bazowego socketu lub szczegóły peera. To właśnie abstrakcja transportu pozwala twojemu kodowi asyncio skupić się wyłącznie na logice danych. Transporty izolują twoją aplikację od chaotycznej mechaniki nieblokującego I/O. Pobierają surowe bajty z twojego protokołu i po cichu obsługują buforowanie, ponowne próby oraz wywołania socketu do systemu operacyjnego, potrzebne do przesłania ich przez sieć. To wszystko w tym odcinku. Dzięki za wysłuchanie i twórzcie dalej!
17

Threading w świecie Async

3m 32s

Połącz światy synchroniczne i asynchroniczne. Dowiedz się, jak bezpiecznie oddelegować ciężki, blokujący kod za pomocą executorów i callbacków thread-safe bez blokowania pętli.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 17 z 20. Wrzucasz standardowy wątek w tle do swojego asynchronicznego serwera webowego, żeby obsłużyć wolne zadanie, a nagle twoja aplikacja łapie deadlocki albo rzuca dziwnymi błędami stanu. Mieszanie standardowych wątków z asynchronicznym event loopem to przepis na katastrofę, chyba że użyjesz dedykowanych mostów thread-safe. Dzisiaj omówimy wątki w świecie async. Podstawową zasadą asyncio jest to, że event loop działa w jednym wątku. Z tego powodu prawie wszystkie obiekty asyncio nie są thread-safe. Częstym błędem jest odpalenie standardowego wątku w tle, zrobienie czegoś, a potem próba zresolwowania asynchronicznego future'a albo zaplanowania callbacka bezpośrednio z tego wątku. Jeśli dotkniesz obiektu asyncio z wątku innego niż ten, w którym działa event loop, uszkodzisz stan pętli. Aby wysłać wiadomość z wątku w tle do twojego event loopa, musisz użyć call soon threadsafe. Jest to metoda na samym event loopie. Przekazujesz jej funkcję callback, którą chcesz uruchomić, oraz argumenty. Zamiast wykonywać ją natychmiast, twój wątek w tle wrzuca ten callback do bezpiecznej wewnętrznej kolejki. Główny event loop sprawdza tę kolejkę i bezpiecznie wykonuje twój callback w głównym wątku podczas swojego normalnego cyklu. To jedyny bezpieczny sposób, w jaki zewnętrzny wątek może szturchnąć event loop. Teraz rozważmy odwrotną sytuację. Uruchamiasz swój asynchroniczny event loop i musisz wykonać kawałek synchronicznego, blokującego kodu. Klasyczny scenariusz to odpytanie wolnego, synchronicznego sterownika PostgreSQL, takiego jak psycopg2. Jeśli wykonasz pięciosekundowe zapytanie do bazy danych bezpośrednio w swoim asynchronicznym request handlerze, cały twój serwer webowy się zatrzyma. Event loop nie może przetwarzać żadnego innego ruchu sieciowego ani timerów, dopóki to zapytanie do bazy danych nie zwróci wyniku. Oto kluczowa sprawa. Aby zapobiec zamrożeniu event loopa, wypychasz tę blokującą pracę do osobnego wątku, używając run in executor. To kolejna metoda na event loopie. Przekazujesz jej thread pool executor i twoją synchroniczną funkcję bazy danych. Event loop przekazuje funkcję do wątku w tle w puli i natychmiast zwraca obiekt awaitable. Robisz await na tym obiekcie. Podczas gdy twoje zapytanie do bazy danych wykonuje się w wątku w tle, twój event loop ma pełną swobodę, by zapauzować to konkretne zadanie i zająć się obsługą setek innych requestów webowych. Kiedy sterownik PostgreSQL w końcu zwróci dane, pula wątków bezpiecznie przekazuje wynik z powrotem do event loopa. Twój obiekt awaitable zostaje zresolwowany, a twoja oryginalna funkcja async wznawia działanie dokładnie tam, gdzie je przerwała, trzymając teraz wyniki z bazy danych. Masz dwa jednokierunkowe mosty. Użyj call soon threadsafe, aby wypchnąć zdarzenia z worker threada do twojego event loopa. Użyj run in executor, aby wypchnąć blokującą, synchroniczną pracę z twojego event loopa do worker threada. Nigdy nie pozwól, aby synchroniczne wywołanie przejęło twój event loop, i nigdy nie pozwalaj, aby wątek w tle bezpośrednio dotykał twoich obiektów async. To wszystko w tym odcinku. Dzięki za wysłuchanie i koduj dalej!
18

Async Generators i czyszczenie

3m 31s

Unikaj wycieków zasobów dzięki async generators. Badamy, dlaczego iteracja async for może pozostawić wiszące połączenia po przerwaniu i jak aclosing() zapewnia bezpieczeństwo.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 18 z 20. Trafiasz na timeout podczas pobierania wierszy z bazy danych. Twój kod obsługuje exception i idzie dalej, ale kilka dni później twoja aplikacja crashuje, bo twój connection pool jest całkowicie wyczerpany. Wyszedłeś z async loopa przedwcześnie, a to po cichu zostawiło otwarte połączenia z bazą danych w tle. Rozwiązanie leży w opanowaniu async generatorów i cleanupu. Kiedy piszesz async generator, żeby robić yield elementów w czasie, często zarządzasz zasobami. Weźmy na przykład kursor bazy danych. Piszesz generator, który pobiera połączenie, robi yield wierszy jeden po drugim i używa bloku try-finally, żeby zwrócić to połączenie do poola, kiedy pobieranie się skończy. Jeśli przeiterujesz przez każdy pojedynczy wiersz, generator kończy działanie, trafia na blok finally i robi cleanup. Wszystko działa. Niebezpieczeństwo pojawia się, gdy nie skonsumujesz całego generatora. Jeśli twoja iteracja jest owrapowana w timeout, albo jeśli po prostu trafisz na instrukcję break po znalezieniu potrzebnego wiersza, generator pauzuje. Jest zawieszony na ostatnim yield. Nie dotarł do bloku finally. Twoje połączenie z bazą danych jest nadal otwarte. Mógłbyś oczekiwać, że garbage collector w Pythonie w końcu sobie z tym poradzi. W synchronicznym kodzie, kiedy generator traci wszystkie referencje i jest zbierany przez garbage collector, Python wstrzykuje exception exit, który uruchamia bloki finally. Ale asynchroniczny kod to komplikuje. Garbage collection to proces synchroniczny. Kiedy garbage collector w końcu znajdzie twój zawieszony async generator, nie może niezawodnie uruchomić asynchronicznego kodu teardown. Event loop może być zajęty, albo może być nawet zamknięty. Poleganie na garbage collectorze do zrobienia cleanupu async generatora skutkuje nieprzewidywalnym zachowaniem i wiszącymi zasobami. To jest ta część, która ma znaczenie. Oficjalna dokumentacja asyncio wyraźnie stwierdza, że nigdy nie powinieneś polegać na garbage collection do cleanupu async generatora. Musisz zamykać je ręcznie. Biblioteka standardowa dostarcza do tego bezpośrednie narzędzie o nazwie aclosing, które znajdziesz w module contextlib. Działa ono jak asynchroniczny context manager. Jego jedynym zadaniem jest zagwarantowanie, że metoda aclose generatora zostanie wywołana z użyciem await w momencie, gdy skończysz z nim pracę. Zamiast wrzucać swój generator bezpośrednio do pętli async for, owrapowujesz go. Najpierw tworzysz instancję generatora. Następnie przekazujesz ją do instrukcji async with aclosing. Wewnątrz tego bloku kontekstu uruchamiasz swoją pętlę async for. Kiedy ustrukturyzujesz swój kod w ten sposób, wczesne wyjście triggeruje context manager. Jeśli timeout przerwie pętlę, blok async with przechwytuje wyjście. Jawnie wywołuje await na metodzie aclose w generatorze. To bezpiecznie wstrzykuje exception exit do zawieszonego generatora, podczas gdy ty nadal aktywnie działasz w event loopie. Twój blok finally wykonuje się natychmiast, robiąc await na wszelkich niezbędnych krokach teardown, a twoje połączenie z bazą danych bezpiecznie wraca do poola. Zawsze, gdy async generator pobiera połączenia sieciowe, deskryptory plików albo blokady bazy danych, owrapuj go w aclosing przed iteracją, żeby zagwarantować deterministyczny cleanup, niezależnie od timeoutów czy wczesnych breaków. To wszystko w tym odcinku. Dzięki za wysłuchanie i koduj dalej!
19

Opanowanie trybu Debug

4m 05s

Błyskawicznie wyłapuj błędy współbieżności. Dowiedz się, jak używać PYTHONASYNCIODEBUG do profilowania wolnych callbacków, odkrywania coroutines bez użycia await i precyzyjnego lokalizowania nigdy nieodebranych wyjątków.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 19 z 20. Twój serwer produkcyjny łapie tajemnicze lag spike'i, a operacje w tle losowo połykają błędy bez śladu. Problemem nie jest logika twojej aplikacji, ale to, jak standardowe asyncio ukrywa błędy współbieżności, żeby oszczędzać na wydajności. Opanowanie Debug Mode to sposób na natychmiastowe obnażenie tych awarii. Debug mode w asyncio działa jak strict mode dla event loopa. Domyślnie asyncio stawia czystą szybkość ponad sprawdzanie bezpieczeństwa w runtime. To oznacza, że kiedy coś idzie nie tak, błędy często wywalają się po cichu. Możesz włączyć debug mode globalnie, ustawiając zmienną środowiskową PYTHONASYNCIODEBUG na jeden, albo uruchamiając Pythona z flagą dash X dev. Możesz go też włączyć dynamicznie, wywołując set debug true bezpośrednio na obiekcie event loopa. Weźmy scenariusz z lag spike'iem. Masz web server obsługujący tysiące współbieżnych requestów i nagle pojedynczy endpoint powoduje zawieszenie całej aplikacji. Podejrzewasz, że jakaś dzika operacja na regexach blokuje wątek, ale standardowe logowanie mówi ci tylko, kiedy request się zaczyna lub kończy, a nie co zablokowało event loopa w międzyczasie. Kiedy debug mode jest aktywny, event loop mierzy czas wykonania każdego pojedynczego callbacka. Jeśli callback zablokuje event loopa na dłużej niż sto milisekund, asyncio automatycznie loguje warning. Ten warning zawiera dokładny plik i numer linijki, w której wystąpił freeze, kierując cię prosto do tego kosztownego wyszukiwania regexem. Ten próg stu milisekund to wartość domyślna, ale możesz go dostosować do swoich konkretnych wymagań dotyczących latency, modyfikując właściwość slow callback duration na event loopie. Debug mode wyłapuje też ciche błędy wykonania. Częstym błędem w asynchronicznym kodzie jest wywołanie funkcji coroutine, ale zapomnienie słowa kluczowego await. Funkcja zwraca obiekt coroutine, ale właściwa logika nigdy się nie odpala. Podczas normalnego wykonania, ten obiekt jest po cichu odrzucany. Debug mode to śledzi. Kiedy garbage collector sprząta coroutine bez awaita, debug loop przechwytuje to i emituje resource warning, pokazując dokładnie, gdzie ta osierocona coroutine została utworzona, żebyś mógł naprawić jej wywołanie. Ta sama siatka bezpieczeństwa dotyczy background tasków. Jeśli task w asyncio zcrashuje, exception jest przechowywany w samym obiekcie taska. Jeśli twój kod nigdy jawnie nie zrobi await na tym tasku ani nie pobierze jego wyniku, exception po prostu znika. Z włączonym debug mode, asyncio monitoruje cykl życia każdego taska. Jeśli task zostanie zniszczony, a jego wewnętrzny exception nigdy nie został pobrany, event loop głośno loguje błąd razem z tracebackiem pokazującym, gdzie ten task został pierwotnie zspawnowany. Te sprawdzenia dodają trochę overheadu, więc zazwyczaj zostawiasz debug mode wyłączony w normalnych środowiskach produkcyjnych, zachowując go na local development albo celowany troubleshooting. Oto kluczowy wniosek. Włączenie debug mode przenosi ciężar znajdowania cichych bugów współbieżności z twojego ręcznego logowania z powrotem na samego event loopa. Jeśli podoba ci się podcast i chcesz nas wesprzeć, po prostu wyszukaj DevStoriesEU na Patreonie. To wszystko w tym odcinku. Dzięki za słuchanie i koduj dalej!
20

Rozszerzanie i niestandardowe Loops

4m 06s

Wielki finał. Omawiamy zaawansowaną integrację i to, czego potrzeba, aby napisać niestandardowy event loop lub stworzyć podklasę BaseEventLoop dla wyspecjalizowanych, wysokowydajnych środowisk.

Pobierz
Cześć, tu Alex z DEV STORIES DOT EU. asyncio, odcinek 20 z 20. Uderzasz w ścianę wydajności w swoim asynchronicznym kodzie, a profilowanie wskazuje bezpośrednio na sam event loop. Nie możesz przepisać biblioteki standardowej, ale potrzebujesz niskopoziomowej kontroli nad tym, jak system obsługuje sockety i taski. Odpowiedź leży w rozszerzaniu i tworzeniu customowych loopów. Standardowy event loop w asyncio nie jest zahardcodowaną czarną skrzynką. To rozszerzalny interfejs. Został zaprojektowany od podstaw tak, aby można go było w pełni zastąpić wysokowydajnymi bibliotekami w C lub specjalistycznymi implementacjami w Pythonie. Większość developerów aplikacji nigdy nie będzie musiała budować customowego loopa. Jeśli jednak jesteś autorem frameworka lub tworzysz zoptymalizowany loop, taki jak uvloop, musisz obejść standardowe zachowanie i zintegrować się bezpośrednio z niskopoziomowymi prymitywami systemu operacyjnego. Aby zbudować customowy event loop, zaczynasz od stworzenia subklasy BaseEventLoop. Ta klasa bazowa definiuje cały kontrakt określający, jak muszą zachowywać się operacje asynchroniczne. Dziedzicząc po niej, dostajesz strukturę, ale możesz nadpisać konkretne metody, aby przechwycić i zdefiniować na nowo podstawowe operacje. Weźmy na przykład tworzenie socketów. W standardowej aplikacji prosisz asyncio o otwarcie połączenia, a ono używa domyślnej pythonowej implementacji socketów. Ale w subklasie customowego loopa możesz nadpisać metody odpowiedzialne za sieć. Oznacza to, że gdy aplikacja żąda połączenia sieciowego, twój customowy loop przechwytuje to wywołanie. Możesz wtedy przepuścić to żądanie przez mocno zoptymalizowany kod w C, albo podpiąć je bezpośrednio pod zaawansowane funkcje kernela, których standardowy Python nie wystawia. Kod aplikacji się nie zmienia, ale mechanika pod spodem jest całkowicie twoja. Ta granularna kontrola dotyczy również zarządzania taskami. I tu jest kluczowa sprawa. Event loop odpowiada za śledzenie każdego pojedynczego asynchronicznego taska. Jeśli zajrzysz pod maskę BaseEventLoop, znajdziesz wewnętrzną metodę o nazwie underscore register task. Nadpisując tę konkretną metodę, twój customowy loop przechwytuje taska dokładnie w mikrosekundzie jego utworzenia. Dlaczego to ma znaczenie? Jeśli budujesz customowy runtime, możesz potrzebować śledzenia głębokiej diagnostyki, zaimplementowania specjalistycznego memory poolingu dla tasków, albo wysyłania stanu taska bezpośrednio do customowego serwisu monitorującego. Nadpisanie underscore register task daje ci gwarantowanego hooka w cykl życia każdej korutyny, zanim w ogóle zacznie się ona wykonywać. Możesz też nadpisać odpowiadającą jej metodę unregister, żeby obsłużyć czyszczenie dokładnie tak, jak wymaga tego twój framework. Kiedy twoja klasa customowego loopa jest już gotowa, musisz powiedzieć Pythonowi, żeby faktycznie jej użył. Robisz to, tworząc customową politykę event loopa. Polityka to po prostu fabryka, która dyktuje, jaka implementacja loopa zostanie utworzona, gdy wątek o nią poprosi. Swoją customową politykę ustawiasz globalnie. Od tego momentu każda funkcja z biblioteki standardowej, która poprosi o event loop, otrzyma twoją zoptymalizowaną, customową wersję. Prawdziwa moc asyncio to nie tylko składnia async i await. To fakt, że cały silnik wykonawczy jest pluggowalnym interfejsem, gotowym do podmiany w momencie, gdy standardowa wydajność zacznie ograniczać twoją architekturę. Ponieważ to kończy naszą serię, zachęcam cię do przeczytania oficjalnej dokumentacji, własnoręcznego spróbowania rozszerzenia tych komponentów, albo odwiedzenia strony devstories dot eu, żeby zasugerować tematy na przyszłe serie. To wszystko w tym odcinku. Dzięki za wysłuchanie i koduj dalej!