Zacznij korzystać z async w Pythonie

Programowanie asynchroniczne lub w skrócie asynchroniczne jest cechą wielu współczesnych języków, która pozwala programowi żonglować wieloma operacjami bez czekania lub rozłączania się z którymkolwiek z nich. Jest to inteligentny sposób na wydajną obsługę zadań, takich jak operacje we / wy w sieci lub plikach, w przypadku których program spędza większość czasu na oczekiwaniu na zakończenie zadania.

Rozważ aplikację do pobierania danych z sieci, która otwiera 100 połączeń sieciowych. Możesz otworzyć jedno połączenie, poczekać na wyniki, a następnie otworzyć następne i czekać na wyniki i tak dalej. Większość czasu działania programu spędza na oczekiwaniu na odpowiedź sieciową, a nie na wykonywaniu rzeczywistej pracy.

Async zapewnia bardziej wydajną metodę: otwórz wszystkie 100 połączeń naraz, a następnie przełączaj się między każdym aktywnym połączeniem, gdy zwracają wyniki. Jeśli jedno połączenie nie zwraca wyników, przełącz się na następne i tak dalej, aż wszystkie połączenia zwrócą swoje dane.

Składnia asynchroniczna jest teraz standardową funkcją w Pythonie, ale długoletni Pythonistas, którzy są przyzwyczajeni do robienia jednej rzeczy na raz, mogą mieć problem z tym, by się nad tym zastanowić. W tym artykule dowiemy się, jak programowanie asynchroniczne działa w Pythonie i jak go używać.

Zwróć uwagę, że jeśli chcesz używać asynchronicznego w Pythonie, najlepiej jest używać Pythona 3.7 lub Pythona 3.8 (najnowsza wersja w chwili pisania tego tekstu). Będziemy używać asynchronicznej składni i funkcji pomocniczych Pythona, zgodnie z definicją w tych wersjach języka.

Kiedy używać programowania asynchronicznego

Ogólnie rzecz biorąc, najlepszy czas na użycie asynchroniczności to próba wykonania pracy, która ma następujące cechy:

  • Wykonanie tej pracy zajmuje dużo czasu.
  • Opóźnienie polega na oczekiwaniu na operacje we / wy (dysku lub sieci), a nie na obliczenia.
  • Praca obejmuje wiele operacji we / wy wykonywanych jednocześnie lub co najmniej jedną operację we / wy, która ma miejsce, gdy próbujesz wykonać inne zadania.

Async umożliwia równoległe konfigurowanie wielu zadań i wydajne iterowanie przez nie bez blokowania reszty aplikacji.

Kilka przykładów zadań, które dobrze współpracują z async:

  • Skrobanie sieci, jak opisano powyżej.
  • Usługi sieciowe (np. Serwer WWW lub framework).
  • Programy koordynujące wyniki z wielu źródeł, których zwrócenie wartości zajmuje dużo czasu (na przykład jednoczesne zapytania do bazy danych).

Należy zauważyć, że programowanie asynchroniczne różni się od wielowątkowości lub przetwarzania wieloprocesowego. Wszystkie operacje asynchroniczne są uruchamiane w tym samym wątku, ale w razie potrzeby ustępują sobie nawzajem, dzięki czemu asynchronizacja jest bardziej wydajna niż wątkowanie lub przetwarzanie wieloprocesowe w przypadku wielu rodzajów zadań. (Więcej na ten temat poniżej).

Python asyncawaitiasyncio

Python niedawno dodał dwa słowa kluczowe asynci awaitdo tworzenia operacji asynchronicznych. Rozważ ten skrypt:

def get_server_status (server_addr) # Potencjalnie długotrwała operacja ... return server_status def server_ops () results = [] results.append (get_server_status ('addr1.server') results.append (get_server_status ('addr2.server') return wyniki 

Asynchroniczna wersja tego samego skryptu - nie funkcjonalna, ale wystarczająca, by dać nam wyobrażenie o działaniu składni - może wyglądać tak.

async def get_server_status (server_addr) # Potencjalnie długotrwała operacja ... return server_status async def server_ops () results = [] results.append (await get_server_status ('addr1.server') results.append (await get_server_status ('addr2. server ') zwracają wyniki 

Funkcje poprzedzone asyncsłowem kluczowym stają się funkcjami asynchronicznymi, znanymi również jako coroutines . Programy zachowują się inaczej niż zwykłe funkcje:

  • Programy mogą używać innego słowa kluczowego, awaitktóre umożliwia programowi oczekiwanie na wyniki z innego programu bez blokowania. Dopóki wyniki nie wrócą z awaitedytora, Python przełącza się swobodnie między innymi uruchomionymi programami.
  • Korekty można wywołać tylko z innych asyncfunkcji. Jeśli uruchomisz server_ops()lub uruchomisz get_server_status()skrypt z treścią skryptu, nie otrzymasz ich wyników; otrzymasz obiekt Coroutine w języku Python, którego nie można użyć bezpośrednio.

Więc jeśli nie możemy wywołać asyncfunkcji z funkcji nieasynchronicznych i nie możemy uruchamiać asyncfunkcji bezpośrednio, jak ich używamy? Odpowiedź: Korzystając z asynciobiblioteki, która łączy asynci resztę Pythona.

Python asyncawaiti asyncioprzykład

Oto przykład (ponownie nie funkcjonalny, ale ilustrujący), jak można napisać aplikację do pobierania plików internetowych przy użyciu asynci asyncio. Ten skrypt pobiera listę adresów URL i używa wielu wystąpień asyncfunkcji z biblioteki zewnętrznej ( read_from_site_async()) do ich pobierania i agregowania wyników.

import asyncio z web_scraping_library import read_from_site_async async def main (url_list): return await asyncio.gather (* [read_from_site_async (_) for _ in url_list]) urls = ['//site1.com','//othersite.com', '//newsite.com'] results = asyncio.run (main (urls)) print (results) 

W powyższym przykładzie używamy dwóch typowych asynciofunkcji:

  • asyncio.run()jest używany do uruchamiania asyncfunkcji z nieasynchronicznej części naszego kodu, a tym samym do uruchamiania wszystkich działań asynchronicznych programu. (Tak właśnie działamy main().)
  • asyncio.gather()pobiera jedną lub więcej funkcji dekorowanych asynchronicznie (w tym przypadku kilka wystąpień read_from_site_async()z naszej hipotetycznej biblioteki do pobierania z sieci), uruchamia je wszystkie i czeka na pojawienie się wszystkich wyników.

Pomysł polega na tym, że rozpoczynamy operację odczytu dla wszystkich witryn naraz, a następnie zbieramy wyniki, gdy dotrą (stąd asyncio.gather()). Nie czekamy na zakończenie jednej operacji, zanim przejdziemy do następnej.

Składniki aplikacji asynchronicznych Pythona

Wspomnieliśmy już, jak aplikacje asynchroniczne Pythona używają coroutines jako głównego składnika, korzystając z asynciobiblioteki, aby je uruchomić. Kilka innych elementów jest również kluczowych dla aplikacji asynchronicznych w Pythonie:

Pętle zdarzeń

asyncioBiblioteka tworzy i zarządza pętli zdarzeń , mechanizmy, które działają współprogram do czasu ukończenia. W procesie Pythona powinna być uruchomiona tylko jedna pętla zdarzeń naraz, choćby po to, by programiście mogli łatwiej śledzić, co się z nią dzieje.

Zadania

Po przesłaniu programu do pętli zdarzeń w celu przetworzenia można odzyskać Taskobiekt, który zapewnia sposób sterowania zachowaniem programu spoza pętli zdarzeń. Jeśli chcesz na przykład anulować uruchomione zadanie, możesz to zrobić, wywołując .cancel()metodę zadania .

Oto nieco inna wersja skryptu site-scraper, który pokazuje pętlę zdarzeń i zadania w działaniu:

import asyncio z web_scraping_library import read_from_site_async task = [] async def main (url_list): for n in url_list: tasks.append (asyncio.create_task (read_from_site_async (n))) print (task) return await asyncio.gls (* tasks.append) = ['//site1.com','//othersite.com','//newsite.com'] loop = asyncio.get_event_loop () results = loop.run_until_complete (main (urls)) print (results) 

Ten skrypt bardziej jawnie używa pętli zdarzeń i obiektów zadań.

  • The .get_event_loop() method provides us with an object that lets us control the event loop directly, by submitting async functions to it programmatically via .run_until_complete(). In the previous script, we could only run a single top-level async function, using asyncio.run(). By the way, .run_until_complete() does exactly what it says: It runs all of the supplied tasks until they’re done, then returns their results in a single batch.
  • The .create_task() method takes a function to run, including its parameters, and gives us back a Task object to run it. Here we submit each URL as a separate Task to the event loop, and store the Task objects in a list. Note that we can only do this inside the event loop—that is, inside an async function.

How much control you need over the event loop and its tasks will depend on how complex the application is that you’re building. If you just want to submit a set of fixed jobs to run concurrently, as with our web scraper, you won’t need a whole lot of control—just enough to launch jobs and gather the results. 

By contrast, if you’re creating a full-blown web framework, you’ll want far more control over the behavior of the coroutines and the event loop. For instance, you may need to shut down the event loop gracefully in the event of an application crash, or run tasks in a threadsafe manner if you’re calling the event loop from another thread.

Async vs. threading vs. multiprocessing

At this point you may be wondering, why use async instead of threads or multiprocessing, both of which have been long available in Python?

First, there is a key difference between async and threads or multiprocessing, even apart from how those things are implemented in Python. Async is about concurrency, while threads and multiprocessing are about parallelism. Concurrency involves dividing time efficiently among multiple tasks at once—e.g., checking your email while waiting for a register at the grocery store. Parallelism involves multiple agents processing multiple tasks side by side—e.g., having five separate registers open at the grocery store.

Most of the time, async is a good substitute for threading as threading is implemented in Python. This is because Python doesn’t use OS threads but its own cooperative threads, where only one thread is ever running at a time in the interpreter. In comparison to cooperative threads, async provides some key advantages:

  • Async functions are far more lightweight than threads. Tens of thousands of asynchronous operations running at once will have far less overhead than tens of thousands of threads.
  • The structure of async code makes it easier to reason about where tasks pick up and leave off. This means data races and thread safety are less of an issue. Because all tasks in the async event loop run in a single thread, it’s easier for Python (and the developer) to serialize how they access objects in memory.
  • Async operations can be cancelled and manipulated more readily than threads. The Task object we get back from asyncio.create_task() provides us with a handy way to do this.

Multiprocessing in Python, on the other hand, is best for jobs that are heavily CPU-bound rather than I/O-bound. Async actually works hand-in-hand with multiprocessing, as you can use asyncio.run_in_executor() to delegate CPU-intensive jobs to a process pool from a central process, without blocking that central process.

Next steps with Python async

The best first thing to do is build a few, simple async apps of your own. Good examples abound now that asynchronous programming in Python has undergone a few versions and had a couple of years to settle down and become more widely used. The official documentation for asyncio is worth reading over to see what it offers, even if you don’t plan to make use of all of its functions.

Możesz również zbadać rosnącą liczbę bibliotek i oprogramowania pośredniego z obsługą asynchronii, z których wiele zapewnia asynchroniczne, nieblokujące wersje łączników bazy danych, protokołów sieciowych i tym podobnych. aio-libsRepozytorium ma kilka najważniejszych z nich, takie jak aiohittpbiblioteki dla dostępu do sieci. Warto też przeszukać Python Package Index pod kątem bibliotek ze asyncsłowem kluczowym. W przypadku czegoś w rodzaju programowania asynchronicznego najlepszym sposobem na naukę jest sprawdzenie, jak inni go używają.