Vabasta samaaegse programmeerimise jõud Pythonis. Õpi Asyncio ülesandeid looma, haldama ja tühistama, et ehitada suure jõudlusega ja skaleeritavaid rakendusi.
Python Asyncio valdamine: süvauurimus ülesannete loomisesse ja haldamisse
Tänapäevase tarkvaraarenduse maailmas on jõudlus ülimalt tähtis. Rakendustelt oodatakse reageerimisvõimet, tuhandete samaaegsete võrguühenduste, andmebaasipäringute ja API-kõnede käsitlemist ilma higi valama. I/O-seotud toimingute puhul – kus programm veedab suurema osa ajast, oodates väliseid ressursse, nagu võrk või ketas – võib traditsiooniline sünkroonne kood muutuda oluliseks kitsaskohaks. Siin paistabki asünkroonne programmeerimine ja Pythoni asyncio
teek on võti selle jõu vabastamiseks.
asyncio
samaaegsusmudeli südames on lihtne, kuid võimas kontseptsioon: Ülesanne. Kui korutiinid määratlevad mida teha, siis Ülesanded on need, mis tegelikult asju ära teevad. Need on samaaegse käivitamise põhiline ühik, mis võimaldab teie Pythoni programmidel žongleerida mitme toiminguga samaaegselt, parandades dramaatiliselt läbilaskevõimet ja reageerimisvõimet.
See põhjalik juhend viib teid sügavale sukeldumisele asyncio.Task
-i. Uurime kõike alates loomise põhitõdedest kuni täiustatud haldusmustrite, tühistamise ja parimate praktikateni. Olenemata sellest, kas ehitate suure liiklusega veebiteenust, andmete kraapimistööriista või reaalajas rakendust, on Ülesannete valdamine oluline oskus igale kaasaegsele Pythoni arendajale.
Mis on korutiin? Kiire kordamine
Enne kui saame joosta, peame kõndima. Ja asyncio
maailmas on kõndimine korutiinide mõistmine. Korutiin on spetsiaalne funktsioonitüüp, mis on määratletud async def
abil.
Kui kutsute välja tavalise Pythoni funktsiooni, käivitatakse see algusest lõpuni. Kui aga kutsute välja korutiinfunktsiooni, ei käivitata seda kohe. Selle asemel tagastab see korutiini objekti. See objekt on plaan tehtava töö jaoks, kuid see on iseenesest inertne. See on peatatud arvutus, mida saab käivitada, peatada ja jätkata.
import asyncio
async def say_hello(name: str):
print(f"Preparing to greet {name}...")
await asyncio.sleep(1) # Simulate a non-blocking I/O operation
print(f"Hello, {name}!")
# Calling the function doesn't run it, it creates a coroutine object
coro = say_hello("World")
print(f"Created a coroutine object: {coro}")
# To actually run it, you need to use an entry point like asyncio.run()
# asyncio.run(coro)
Võlusõna on await
. See ütleb sündmusteahelale: "See toiming võib võtta aega, nii et võite mind siin peatada ja minna midagi muud tegema. Ärge mind unustage kui see toiming on lõpetatud." See võime konteksti peatada ja vahetada võimaldabki samaaegsust.
Samaaegsuse süda: asyncio.Task mõistmine
Niisiis, korutiin on plaan. Kuidas me ütleme köögile (sündmusteahelale), et ta hakkaks kokkama? Siin tuleb appi asyncio.Task
.
asyncio.Task
on objekt, mis pakendab korutiini ja ajastab selle käivitamise asyncio sündmusteahelas. Mõelge sellele nii:
- Korutiin (
async def
): Üksikasjalik retsept roa jaoks. - Sündmusteahel: Keskne köök, kus kõik küpsetamine toimub.
await my_coro()
: Sa seisad köögis ja järgid retsepti samm-sammult ise. Sa ei saa midagi muud teha enne, kui roog on valmis. See on järjestikune käivitamine.asyncio.create_task(my_coro())
: Sa annad retsepti kokale (Ülesanne) köögis ja ütled: "Hakka sellega tegelema." Kokk alustab kohe ja sa oled vaba tegema muid asju, näiteks jagama rohkem retsepte. See on samaaegne käivitamine.
Põhiline erinevus on see, et asyncio.create_task()
ajastab korutiini taustal käivitamiseks ja tagastab kohe kontrolli teie koodile. Saate tagasi Task
objekti, mis toimib selle käimasoleva toimingu juhtimispuldina. Saate seda juhtimispulti kasutada selle oleku kontrollimiseks, selle tühistamiseks või hiljem selle tulemuse ootamiseks.
Esimeste ülesannete loomine: `asyncio.create_task()` funktsioon
Peamine viis Ülesande loomiseks on asyncio.create_task()
funktsioon. See võtab argumendina korutiini objekti ja ajastab selle käivitamiseks.
Põhiline süntaks
Kasutamine on lihtne:
import asyncio
async def my_background_work():
print("Starting background work...")
await asyncio.sleep(2)
print("Background work finished.")
return "Success"
async def main():
print("Main function started.")
# Schedule my_background_work to run concurrently
task = asyncio.create_task(my_background_work())
# While the task runs, we can do other things
print("Task created. Main function continues to run.")
await asyncio.sleep(1)
print("Main function did some other work.")
# Now, wait for the task to complete and get its result
result = await task
print(f"Task completed with result: {result}")
asyncio.run(main())
Pange tähele, kuidas väljund näitab, et `main` funktsioon jätkab käivitamist kohe pärast ülesande loomist. See ei blokeeri. See peatub ainult siis, kui me lõpus selgesõnaliselt `await task`.
Praktiline näide: Samaaegsed veebipäringud
Vaatame Ülesannete tegelikku jõudu levinud stsenaariumiga: andmete hankimine mitmelt URL-ilt. Selleks kasutame populaarset aiohttp
teeki, mille saate installida käsuga `pip install aiohttp`.
Esiteks vaatame järjestikust (aeglast) viisi:
import asyncio
import aiohttp
import time
async def fetch_status(session, url):
async with session.get(url) as response:
return response.status
async def main_sequential():
urls = [
"https://www.python.org",
"https://www.google.com",
"https://www.github.com",
"https://www.microsoft.com"
]
start_time = time.time()
async with aiohttp.ClientSession() as session:
for url in urls:
status = await fetch_status(session, url)
print(f"Status for {url}: {status}")
end_time = time.time()
print(f"Sequential execution took {end_time - start_time:.2f} seconds")
# To run this, you would use: asyncio.run(main_sequential())
Kui iga päring võtab aega umbes 0,5 sekundit, on koguaeg umbes 2 sekundit, sest iga `await` blokeerib silmuse seni, kuni see üksik päring on lõpetatud.
Nüüd vabastame samaaegsuse jõu Ülesannetega:
import asyncio
import aiohttp
import time
# fetch_status coroutine remains the same
async def fetch_status(session, url):
async with session.get(url) as response:
return response.status
async def main_concurrent():
urls = [
"https://www.python.org",
"https://www.google.com",
"https://www.github.com",
"https://www.microsoft.com"
]
start_time = time.time()
async with aiohttp.ClientSession() as session:
# Create a list of tasks, but don't await them yet
tasks = [asyncio.create_task(fetch_status(session, url)) for url in urls]
# Now, wait for all tasks to complete
statuses = await asyncio.gather(*tasks)
for url, status in zip(urls, statuses):
print(f"Status for {url}: {status}")
end_time = time.time()
print(f"Concurrent execution took {end_time - start_time:.2f} seconds")
asyncio.run(main_concurrent())
Kui käivitate samaaegse versiooni, näete dramaatilist erinevust. Koguaeg on ligikaudu pikima üksiku päringu aeg, mitte kõigi nende summa. Seda seetõttu, et niipea kui esimene `fetch_status` korutiin jõuab oma `await session.get(url)`-i, peatab sündmusteahel selle ja käivitab kohe järgmise. Kõik võrgupäringud toimuvad sisuliselt samal ajal.
Ülesannete grupi haldamine: olulised mustrid
Üksikute ülesannete loomine on suurepärane, kuid reaalses maailmas peate sageli käivitama, haldama ja sünkroonima terve grupi. asyncio
pakub selleks mitmeid võimsaid tööriistu.
Kaasaegne lähenemine (Python 3.11+): `asyncio.TaskGroup`
Python 3.11-s tutvustatud TaskGroup
on uus, soovitatav ja ohutum viis seotud ülesannete grupi haldamiseks. See pakub seda, mida tuntakse kui struktureeritud samaaegsust.
TaskGroup
põhifunktsioonid:
- Garanteeritud puhastamine:
async with
plokk ei välju enne, kui kõik selles loodud ülesanded on lõpetatud. - Tugev veakäsitlus: Kui mõni grupi ülesanne tõstab erandi, tühistatakse automaatselt kõik teised grupi ülesanded ja erand (või `ExceptionGroup`) tõstetakse uuesti
async with
ploki väljumisel. See hoiab ära orbude ülesanded ja tagab ennustatava oleku.
Siin on, kuidas seda kasutada:
import asyncio
async def worker(delay):
print(f"Worker starting, will sleep for {delay}s")
await asyncio.sleep(delay)
# This worker will fail
if delay == 2:
raise ValueError("Something went wrong in worker 2")
print(f"Worker with delay {delay} finished")
return f"Result from {delay}s"
async def main():
print("Starting main with TaskGroup...")
try:
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(worker(1))
task2 = tg.create_task(worker(2)) # This one will fail
task3 = tg.create_task(worker(3))
print("Tasks created in the group.")
# This part of the code will NOT be reached if an exception occurs
# The results would be accessed via task1.result(), etc.
print("All tasks completed successfully.")
except* ValueError as eg: # Note the `except*` for ExceptionGroup
print(f"Caught an exception group with {len(eg.exceptions)} exceptions.")
for exc in eg.exceptions:
print(f" - {exc}")
print("Main function finished.")
asyncio.run(main())
Kui seda käivitate, näete, et `worker(2)` tõstab vea. `TaskGroup` püüab selle kinni, tühistab teised töötavad ülesanded (nagu `worker(3)`) ja seejärel tõstab `ExceptionGroup`, mis sisaldab `ValueError`. See muster on uskumatu vastupidav usaldusväärsete süsteemide ehitamiseks.
Klassikaline tööloom: `asyncio.gather()`
Enne `TaskGroup` oli `asyncio.gather()` kõige levinum viis mitme ootamatuga samaaegseks käivitamiseks ja nende kõigi lõpetamise ootamiseks.
gather()` võtab ootamatute või Ülesannete jada, käivitab need kõik ja tagastab nende tulemuste loendi samas järjekorras nagu sisendid. See on kõrgetasemeline ja mugav funktsioon levinud juhul, kui "käivitage kõik need asjad ja andke mulle kõik tulemused."
import asyncio
async def fetch_data(source, delay):
print(f"Fetching from {source}...")
await asyncio.sleep(delay)
return {"source": source, "data": f"some data from {source}"}
async def main():
# gather can take coroutines directly
results = await asyncio.gather(
fetch_data("API", 2),
fetch_data("Database", 3),
fetch_data("Cache", 1)
)
print(results)
asyncio.run(main())
Veakäsitlus koos `gather()`: Vaikimisi, kui mõni `gather()`-le edastatud oodatavatest tõstab erandi, levitab `gather()` selle erandi kohe ja teised töötavad ülesanded tühistatakse. Saate seda käitumist muuta väärtusega `return_exceptions=True`. Selles režiimis paigutatakse erandi tõstmise asemel see tulemuste loendisse vastavasse asukohta.
# ... inside main()
results = await asyncio.gather(
fetch_data("API", 2),
asyncio.create_task(worker(1)), # This will raise a ValueError
fetch_data("Cache", 1),
return_exceptions=True
)
# results will contain a mix of successful results and exception objects
print(results)
Peenhäälestatud juhtimine: `asyncio.wait()`
asyncio.wait()` on madalama taseme funktsioon, mis pakub üksikasjalikumat kontrolli ülesannete grupi üle. Erinevalt `gather()`-ist ei tagasta see tulemusi otse. Selle asemel tagastab see kaks ülesannete hulka: `done` ja `pending`.
Selle kõige võimsam funktsioon on `return_when` parameeter, mis võib olla:
asyncio.ALL_COMPLETED
(vaikimisi): Tagastab, kui kõik ülesanded on lõpetatud.asyncio.FIRST_COMPLETED
: Tagastab niipea, kui vähemalt üks ülesanne on lõpetatud.asyncio.FIRST_EXCEPTION
: Tagastab, kui ülesanne tõstab erandi. Kui ükski ülesanne ei tõsta erandit, on see samaväärne `ALL_COMPLETED`-ga.
See on äärmiselt kasulik stsenaariumide puhul, nagu mitme üleliigse andmeallika küsimine ja esimese reageerija kasutamine:
import asyncio
async def query_source(name, delay):
await asyncio.sleep(delay)
return f"Result from {name}"
async def main():
tasks = [
asyncio.create_task(query_source("Fast Mirror", 0.5)),
asyncio.create_task(query_source("Slow Main DB", 2.0)),
asyncio.create_task(query_source("Geographic Replica", 0.8))
]
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
# Get the result from the completed task
first_result = done.pop().result()
print(f"Got first result: {first_result}")
# We now have pending tasks that are still running. It's crucial to clean them up!
print(f"Cancelling {len(pending)} pending tasks...")
for task in pending:
task.cancel()
# Await the cancelled tasks to allow them to process the cancellation
await asyncio.gather(*pending, return_exceptions=True)
print("Cleanup complete.")
asyncio.run(main())
TaskGroup vs. gather() vs. wait(): Millal mida kasutada?
- Kasutage `asyncio.TaskGroup` (Python 3.11+) oma vaikevalikuna. Selle struktureeritud samaaegsusmudel on ohutum, puhtam ja vähem veaohtlik ühe loogilise toimingu alla kuuluvate ülesannete grupi haldamiseks.
- Kasutage `asyncio.gather()`, kui peate käivitama sõltumatute ülesannete grupi ja soovite lihtsalt nende tulemuste loendit. See on endiselt väga kasulik ja veidi lühem lihtsate juhtumite puhul, eriti Pythoni versioonides enne 3.11.
- Kasutage `asyncio.wait()` täiustatud stsenaariumide puhul, kus vajate peenhäälestatud kontrolli lõpetamistingimuste üle (nt esimese tulemuse ootamine) ja olete valmis käsitsi haldama ülejäänud ootel ülesandeid.
Ülesande elutsükkel ja haldamine
Kui Ülesanne on loodud, saate sellega suhelda, kasutades `Task` objekti meetodeid.
Ülesande oleku kontrollimine
task.done()
: Tagastab `True`, kui ülesanne on lõpetatud (kas edukalt, erandiga või tühistamisega).task.cancelled()
: Tagastab `True`, kui ülesanne tühistati.task.exception()
: Kui ülesanne tõstis erandi, tagastab see erandi objekti. Muidu tagastab see `None`. Saate seda kutsuda ainult pärast seda, kui ülesanne on `done()`.
Tulemuste hankimine
Peamine viis ülesande tulemuse hankimiseks on lihtsalt `await task`. Kui ülesanne lõppes edukalt, tagastab see väärtuse. Kui see tõstis erandi, siis `await task` tõstab selle erandi uuesti. Kui see tühistati, siis `await task` tõstab `CancelledError`.
Alternatiivina, kui teate, et ülesanne on `done()`, saate kutsuda `task.result()`. See käitub samamoodi nagu `await task` väärtuste tagastamise või erandite tõstmise osas.
Tühistamise kunst
Pikalt kestvate toimingute graatsiliselt tühistamise võime on kriitiline vastupidavate rakenduste ehitamiseks. Teil võib tekkida vajadus ülesanne tühistada ajalõpu, kasutajapäringu või süsteemi mujal asuva vea tõttu.
Ülesande tühistamiseks kutsuge välja selle task.cancel()
meetod. See aga ei peata ülesannet kohe. Selle asemel ajastab see CancelledError
erandi viskamise korutiini sees järgmisel await
punktis. See on oluline detail. See annab korutiinile võimaluse enne väljumist puhastada.
Hästi käituv korutiin peaks seda `CancelledError` graatsiliselt käsitlema, tavaliselt kasutades `try...finally` plokki, et tagada ressursside, nagu failikäepidemed või andmebaasiühendused, sulgemine.
import asyncio
async def resource_intensive_task():
print("Acquiring resource (e.g., opening a connection)...")
try:
for i in range(10):
print(f"Working... step {i+1}")
await asyncio.sleep(1) # This is an await point where CancelledError can be injected
except asyncio.CancelledError:
print("Task was cancelled! Cleaning up...")
raise # It's good practice to re-raise CancelledError
finally:
print("Releasing resource (e.g., closing connection). This always runs.")
async def main():
task = asyncio.create_task(resource_intensive_task())
# Let it run for a bit
await asyncio.sleep(2.5)
print("Main decides to cancel the task.")
task.cancel()
try:
await task
except asyncio.CancelledError:
print("Main has confirmed the task was cancelled.")
asyncio.run(main())
`finally` plokk käivitatakse garanteeritult, muutes selle ideaalseks kohaks puhastusloogika jaoks.
Ajalõppude lisamine koos `asyncio.timeout()` ja `asyncio.wait_for()`
Käsitsi magamine ja tühistamine on tüütu. `asyncio` pakub selle tavalise mustri jaoks abilisi.
Python 3.11+ on eelistatud viis kasutada `asyncio.timeout()` kontekstihaldurit:
async def long_running_operation():
await asyncio.sleep(10)
print("Operation finished")
async def main():
try:
async with asyncio.timeout(2): # Set a 2-second timeout
await long_running_operation()
except TimeoutError:
print("The operation timed out!")
asyncio.run(main())
Vanemate Pythoni versioonide jaoks saate kasutada `asyncio.wait_for()`. See töötab sarnaselt, kuid pakendab oodatava funktsioonikõnesse:
async def main_legacy():
try:
await asyncio.wait_for(long_running_operation(), timeout=2)
except asyncio.TimeoutError:
print("The operation timed out!")
asyncio.run(main_legacy())
Mõlemad tööriistad töötavad sisemise ülesande tühistamisega, kui ajalõpp on saavutatud, tõstes `TimeoutError` (mis on `CancelledError` alamklass).
Levinud lõkse ja parimad praktikad
Ülesannetega töötamine on võimas, kuid on mõned levinud lõksud, mida vältida.
- Lõks: "Tulekahju ja unusta" viga. Ülesande loomine käsuga `create_task` ja seejärel selle kunagi ootamine (või haldur nagu `TaskGroup`) on ohtlik. Kui see ülesanne tõstab erandi, võib erand vaikselt kaduma minna ja teie programm võib väljuda enne, kui ülesanne isegi oma töö lõpetab. Igal ülesandel peaks alati olema selge omanik, kes vastutab selle tulemuse ootamise eest.
- Lõks: `asyncio.run()` segiajamine `create_task()`-ga. `asyncio.run(my_coro())` on peamine sisenemispunkt `asyncio` programmi käivitamiseks. See loob uue sündmusteahela ja käitab antud korutiini kuni selle lõpetamiseni. `asyncio.create_task(my_coro())` kasutatakse juba käivitatavas asünkroonfunktsioonis samaaegse käivitamise ajastamiseks.
- Parim tava: Kasutage `TaskGroup` kaasaegse Pythoni jaoks. Selle disain hoiab ära paljud levinud vead, nagu unustatud ülesanded ja käsitlemata erandid. Kui kasutate Python 3.11 või uuemat, tehke see oma vaikevalikuks.
- Parim tava: Nimetage oma ülesanded. Ülesande loomisel kasutage `name` parameetrit: `asyncio.create_task(my_coro(), name='DataProcessor-123')`. See on hindamatu silumiseks. Kui loetlete kõik töötavad ülesanded, aitab mõttekate nimede olemasolu mõista, mida teie programm teeb.
- Parim tava: Tagage graatsiline väljalülitamine. Kui teie rakendus peab sulgema, veenduge, et teil on mehhanism kõigi töötavate taustaülesannete tühistamiseks ja oodake, kuni need korralikult puhastavad.
Täiustatud kontseptsioonid: pilguheit kaugemale
Silumiseks ja introspektsiooniks pakub `asyncio` paari kasulikku funktsiooni:
asyncio.current_task()
: Tagastab praegu käitatava koodi `Task` objekti.asyncio.all_tasks()
: Tagastab kõigi `Task` objektide hulga, mida sündmusteahel praegu haldab. See sobib suurepäraselt silumiseks, et näha, mis töötab.
Saate ülesannetele lisada ka lõpetamis tagasikutsumised, kasutades `task.add_done_callback()`. Kuigi see võib olla kasulik, viib see sageli keerukama tagasikutsumisstiilis koodistruktuurini. Kaasaegsed lähenemisviisid, mis kasutavad `await`, `TaskGroup` või `gather`, on üldiselt eelistatud loetavuse ja hooldatavuse tagamiseks.
Järeldus
asyncio.Task
on kaasaegse Pythoni samaaegsuse mootor. Mõistes, kuidas luua, hallata ja graatsiliselt käsitleda ülesannete elutsüklit, saate muuta oma I/O-seotud rakendused aeglastest, järjestikustest protsessidest väga tõhusateks, skaleeritavateks ja reageerimisvõimelisteks süsteemideks.
Oleme käsitlenud teekonda alates korutiini ajastamise põhimõttest käsuga `create_task()` kuni keerukate töövoogude orkestreerimiseni koos `TaskGroup`, `gather()` ja `wait()`. Oleme uurinud ka vastupidava tarkvara ehitamiseks kriitilise tähtsusega jõulist veakäsitlust, tühistamist ja ajalõppe.
Asünkroonse programmeerimise maailm on tohutu, kuid Ülesannete valdamine on kõige olulisem samm, mida saate teha. Alustage katsetamist. Teisendage oma rakenduse järjestikune I/O-seotud osa samaaegsete ülesannete kasutamiseks ja tunnistage jõudluse kasvu ise. Võtke omaks samaaegsuse jõud ja olete hästi varustatud järgmise põlvkonna suure jõudlusega Pythoni rakenduste ehitamiseks.