Atskleiskite lygiagretaus programavimo galią Python kalba. Išmokite kurti, valdyti ir atšaukti „Asyncio“ užduotis, kad kurtumėte didelio našumo, mastelį didinančias programas.
„Python Asyncio“ įvaldymas: išsamus užduočių kūrimo ir valdymo gidas
Šiuolaikiniame programinės įrangos kūrimo pasaulyje našumas yra svarbiausias. Tikimasi, kad programos bus reaguojančios, apdorojančios tūkstančius lygiagrečių tinklo jungčių, duomenų bazių užklausų ir API iškvietimų be menkiausio vargo. I/O operacijoms, kurioms programa didžiąją dalį laiko praleidžia laukdama išorinių išteklių, tokių kaip tinklas ar diskas, tradicinis sinchroninis kodas gali tapti dideliu trikdžiu. Čia pasireiškia asinchroninis programavimas, o Python asyncio
biblioteka yra raktas į šios galios atskleidimą.
Pačioje asyncio
lygiagretumo modelio širdyje slypi paprasta, bet galinga koncepcija: užduotis (angl. Task). Nors korutinos apibrėžia ką daryti, užduotys yra tai, kas iš tikrųjų atlieka darbus. Jos yra pagrindinis lygiagretaus vykdymo vienetas, leidžiantis jūsų Python programoms vienu metu apdoroti kelias operacijas, žymiai pagerinant pralaidumą ir reakciją.
Šis išsamus vadovas leis jums išsamiai pasinerti į asyncio.Task
. Išnagrinėsime viską nuo kūrimo pagrindų iki pažangių valdymo modelių, atšaukimo ir geriausių praktikų. Nesvarbu, ar kuriate didelio srauto žiniatinklio paslaugą, duomenų rinkimo įrankį ar realaus laiko programą, užduočių įvaldymas yra esminis įgūdis bet kuriam šiuolaikiniam Python kūrėjui.
Kas yra korutina? Trumpas prisiminimas
Prieš pradedant bėgti, turime išmokti vaikščioti. O asyncio
pasaulyje vaikščiojimas yra korutinų supratimas. Korutina yra specialaus tipo funkcija, apibrėžiama su async def
.
Kai iškviečiate įprastą Python funkciją, ji vykdoma nuo pradžios iki pabaigos. Tačiau iškvietus korutinos funkciją, ji nevykdoma iškart. Vietoj to, ji grąžina korutinos objektą. Šis objektas yra atliktino darbo planas, bet pats savaime yra neveiklus. Tai sustabdytas skaičiavimas, kurį galima pradėti, sustabdyti ir atnaujinti.
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)
Magijos raktažodis yra await
. Jis nurodo įvykių ciklui: "Ši operacija gali užtrukti, todėl drąsiai sustabdyk mane čia ir dirbk ką nors kita. Pažadink mane, kai ši operacija bus baigta." Šis gebėjimas sustabdyti ir perjungti kontekstus leidžia pasiekti lygiagretumą.
Lygiagretumo širdis: „asyncio.Task“ supratimas
Taigi, korutina yra planas. Kaip liepiame virtuvei (įvykių ciklui) pradėti gaminti? Čia atsiranda asyncio.Task
.
asyncio.Task
yra objektas, apgaubiantis korutiną ir suplanuojantis ją vykdymui asyncio
įvykių cikle. Pagalvokite apie tai taip:
- Korutina (
async def
): išsamus patiekalo receptas. - Įvykių ciklas (angl. Event Loop): Centrinė virtuvė, kurioje vyksta visas gaminimas.
await my_coro()
: Jūs stovite virtuvėje ir pats žingsnis po žingsnio vykdote receptą. Nieko kito negalite daryti, kol patiekalas nėra baigtas. Tai yra nuoseklus vykdymas.asyncio.create_task(my_coro())
: Jūs paduodate receptą virėjui (užduočiai) virtuvėje ir sakote: "Pradėkite dirbti prie to." Virėjas iškart pradeda, o jūs galite laisvai daryti kitus dalykus, pavyzdžiui, dalyti daugiau receptų. Tai yra lygiagretus vykdymas.
Pagrindinis skirtumas yra tas, kad asyncio.create_task()
suplanuoja korutinos vykdymą "foniniu režimu" ir nedelsiant grąžina valdymą jūsų kodui. Jūs gaunate Task
objektą, kuris veikia kaip šios vykdomos operacijos rankena. Šią rankeną galite naudoti norėdami patikrinti jos būseną, atšaukti ją arba vėliau laukti jos rezultato.
Pirmųjų užduočių kūrimas: funkcija `asyncio.create_task()`
Pagrindinis būdas sukurti užduotį yra naudojant funkciją asyncio.create_task()
. Ji priima korutinos objektą kaip argumentą ir suplanuoja jį vykdymui.
Pagrindinė sintaksė
Naudojimas yra paprastas:
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())
Atkreipkite dėmesį, kaip išvestis rodo, kad funkcija `main` tęsia savo vykdymą iškart po užduoties sukūrimo. Ji neužstringa. Ji pristabdoma tik tada, kai pabaigoje aiškiai naudojame `await task`.
Praktinis pavyzdys: lygiagretūs žiniatinklio užklausos
Pažiūrėkime tikrąją užduočių galią su dažnu scenarijumi: duomenų gavimas iš kelių URL. Tam naudosime populiarią `aiohttp` biblioteką, kurią galite įdiegti naudodami `pip install aiohttp`.
Pirmiausia, pažiūrėkime nuoseklų (lėtą) būdą:
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())
Jei kiekviena užklausa trunka apie 0,5 sekundės, bendras laikas bus maždaug 2 sekundės, nes kiekvienas `await` užblokuoja ciklą, kol baigiama ta viena užklausa.
Dabar atskleiskime lygiagretumo galią su užduotimis:
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())
Paleidus lygiagrečią versiją, pamatysite dramatišką skirtumą. Bendras laikas bus maždaug ilgiausios pavienės užklausos laikas, o ne visų jų suma. Taip yra todėl, kad vos tik pirmoji `fetch_status` korutina pasiekia savo `await session.get(url)`, įvykių ciklas ją sustabdo ir nedelsiant pradeda kitą. Visos tinklo užklausos vyksta efektyviai vienu metu.
Užduočių grupės valdymas: esminiai modeliai
Atskirų užduočių kūrimas yra puiku, tačiau realiose programose dažnai reikia paleisti, valdyti ir sinchronizuoti visą jų grupę. `asyncio` tam suteikia keletą galingų įrankių.
Šiuolaikinis požiūris (Python 3.11+): `asyncio.TaskGroup`
Pristatytas su Python 3.11, `TaskGroup` yra naujas, rekomenduojamas ir saugiausias būdas valdyti susijusių užduočių grupę. Jis suteikia tai, kas žinoma kaip struktūrizuotas lygiagretumas.
Pagrindinės `TaskGroup` savybės:
- Garantuotas išvalymas: `async with` blokas neišeis, kol nebus baigtos visos jame sukurtos užduotys.
- Patikimas klaidų tvarkymas: Jei bet kuri grupės užduotis iškelia išimtį, visos kitos grupės užduotys bus automatiškai atšauktos, o išimtis (arba `ExceptionGroup`) bus vėl iškelta išeinant iš `async with` bloko. Tai apsaugo nuo našlaičių užduočių ir užtikrina nuspėjamą būseną.
Štai kaip jį naudoti:
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())
Kai tai paleisite, pamatysite, kad `worker(2)` iškelia klaidą. `TaskGroup` ją pagauna, atšaukia kitas vykdomas užduotis (pvz., `worker(3)`) ir tada iškelia `ExceptionGroup`, kuriame yra `ValueError`. Šis modelis yra nepaprastai patikimas kuriant patikimas sistemas.
Klasikinis darbinis arklys: `asyncio.gather()`
Prieš `TaskGroup`, `asyncio.gather()` buvo pats dažniausias būdas vienu metu vykdyti kelis laukiamus (angl. awaitable) elementus ir laukti, kol visi jie baigsis.
gather()
paima korutinų arba užduočių seką, vykdo jas visas ir grąžina jų rezultatų sąrašą ta pačia tvarka, kaip ir įvesties elementai. Tai aukšto lygio, patogi funkcija, skirta įprastam atvejui "paleisti visus šiuos dalykus ir pateikti man visus rezultatus"
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())
Klaidų tvarkymas naudojant `gather()`: Pagal numatytuosius nustatymus, jei bet kuris `gather()` perduotas laukiamas elementas iškelia išimtį, `gather()` nedelsiant perduoda tą išimtį, o kitos vykdomos užduotys atšaukiamos. Šį elgesį galite pakeisti naudodami `return_exceptions=True`. Šiuo režimu, užuot iškėlus išimtį, ji bus įdėta į rezultatų sąrašą atitinkamoje pozicijoje.
# ... 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)
Smulkus valdymas: `asyncio.wait()`
asyncio.wait()
yra žemesnio lygio funkcija, suteikianti išsamesnį užduočių grupės valdymą. Skirtingai nei `gather()`, ji tiesiogiai negrąžina rezultatų. Vietoj to, ji grąžina du užduočių rinkinius: `done` (baigtas) ir `pending` (laukiančias).
Galingiausia jo funkcija yra parametras `return_when`, kuris gali būti:
asyncio.ALL_COMPLETED
(numatytasis): Grįžta, kai baigtos visos užduotys.asyncio.FIRST_COMPLETED
: Grįžta, kai tik bent viena užduotis baigiama.asyncio.FIRST_EXCEPTION
: Grįžta, kai užduotis iškelia išimtį. Jei jokia užduotis neiškelia išimties, tai atitinka `ALL_COMPLETED`.
Tai yra nepaprastai naudinga scenarijams, tokiems kaip kelių perteklinių duomenų šaltinių užklausos ir pirmojo, kuris atsako, naudojimas:
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(): kada ką naudoti?
- Naudokite `asyncio.TaskGroup` (Python 3.11+) kaip numatytąjį pasirinkimą. Jo struktūrizuotas lygiagretumo modelis yra saugesnis, švaresnis ir mažiau linkęs klaidoms valdant užduočių grupę, kuri priklauso vienai loginei operacijai.
- Naudokite `asyncio.gather()`, kai reikia paleisti nepriklausomų užduočių grupę ir tiesiog norite gauti jų rezultatų sąrašą. Jis vis dar labai naudingas ir šiek tiek glaustesnis paprastesniais atvejais, ypač Python versijose iki 3.11.
- Naudokite `asyncio.wait()` pažangiems scenarijams, kai reikia smulkaus valdymo, susijusio su užbaigimo sąlygomis (pvz., laukti pirmojo rezultato) ir esate pasiruošę rankiniu būdu valdyti likusias laukiančias užduotis.
Užduoties gyvavimo ciklas ir valdymas
Sukūrus užduotį, galite su ja bendrauti naudodami `Task` objekto metodus.
Užduoties būsenos tikrinimas
task.done()
: Grąžina `True`, jei užduotis baigta (sėkmingai, su išimtimi arba atšaukus).task.cancelled()
: Grąžina `True`, jei užduotis buvo atšaukta.task.exception()
: Jei užduotis iškėlė išimtį, šis metodas grąžina išimties objektą. Priešingu atveju, grąžina `None`. Šį metodą galite kviesti tik tada, kai užduotis yra `done()`.
Rezultatų gavimas
Pagrindinis būdas gauti užduoties rezultatą yra tiesiog `await task`. Jei užduotis baigėsi sėkmingai, grąžinama reikšmė. Jei ji iškėlė išimtį, `await task` vėl iškels tą išimtį. Jei ji buvo atšaukta, `await task` iškels `CancelledError`.
Arba, jei žinote, kad užduotis yra `done()`, galite iškviesti `task.result()`. Tai elgiasi identiškai kaip `await task` kalbant apie reikšmių grąžinimą ar išimčių kėlimą.
Atšaukimo menas
Gebėjimas grakščiai atšaukti ilgai trunkančias operacijas yra gyvybiškai svarbus kuriant patikimas programas. Gali tekti atšaukti užduotį dėl laiko limito, vartotojo užklausos ar klaidos kitoje sistemos vietoje.
Užduotį atšaukiate iškviesdami jos metodą task.cancel()
. Tačiau tai iškart nesustoja užduoties. Vietoj to, ji suplanuoja, kad `CancelledError` išimtis būtų iškelta korutinos viduje kitame await
taške. Tai yra esminė detalė. Ji suteikia korutinai galimybę išsivalyti prieš išeinant.
Tinkamai veikianti korutina turėtų grakščiai tvarkyti šią `CancelledError` išimtį, paprastai naudodama `try...finally` bloką, siekiant užtikrinti, kad ištekliai, tokie kaip failų rankenos ar duomenų bazių jungtys, būtų uždaryti.
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())
The `finally` block is guaranteed to execute, making it the perfect place for cleanup logic.
Laiko limitų pridėjimas su `asyncio.timeout()` ir `asyncio.wait_for()`
Rankinis laukimas ir atšaukimas yra varginantis. `asyncio` suteikia pagalbos priemones šiam dažnam modeliui.
Python 3.11+ versijoje `asyncio.timeout()` konteksto tvarkyklė yra pageidaujamas būdas:
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())
Senesnėms Python versijoms galite naudoti `asyncio.wait_for()`. Jis veikia panašiai, bet apvynioja laukiamą objektą funkcijos iškvietimu:
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())
Abu įrankiai veikia atšaukdami vidinę užduotį, kai pasiekiamas laiko limitas, ir iškeldami `TimeoutError` (kuri yra `CancelledError` subklasė).
Dažniausios klaidos ir geriausios praktikos
Darbas su užduotimis yra galingas, tačiau yra keletas dažnų spąstų, kurių reikėtų vengti.
- Spąstai: „Paleisk ir pamiršk“ klaida. Sukurti užduotį su `create_task` ir niekada jos nelaukti (arba naudoti tvarkyklę, pvz., `TaskGroup`) yra pavojinga. Jei ta užduotis iškelia išimtį, išimtis gali būti tyliai prarasta, o jūsų programa gali išeiti dar prieš užduočiai baigus darbą. Visada turėkite aiškų kiekvienos užduoties savininką, kuris yra atsakingas už jos rezultato laukimą.
- Spąstai: `asyncio.run()` painiojimas su `create_task()`. `asyncio.run(my_coro())` yra pagrindinis įėjimo taškas `asyncio` programai paleisti. Jis sukuria naują įvykių ciklą ir vykdo duotą korutiną, kol ji bus baigta. `asyncio.create_task(my_coro())` naudojamas jau veikiančioje asinchroninėje funkcijoje, siekiant suplanuoti lygiagretų vykdymą.
- Geriausia praktika: naudokite `TaskGroup` šiuolaikinei Python. Jo dizainas apsaugo nuo daugelio dažnų klaidų, tokių kaip pamirštos užduotys ir neapdorotos išimtys. Jei naudojate Python 3.11 ar naujesnę versiją, padarykite tai savo numatytuoju pasirinkimu.
- Geriausia praktika: pavadinkite savo užduotis. Kuriant užduotį, naudokite parametrą `name`: `asyncio.create_task(my_coro(), name='DataProcessor-123')`. Tai yra neįkainojama derinimui. Kai išvardinate visas vykdomas užduotis, turint prasmingus pavadinimus, padeda suprasti, ką daro jūsų programa.
- Geriausia praktika: užtikrinkite grakštų išjungimą. Kai jūsų programai reikia išjungti, įsitikinkite, kad turite mechanizmą atšaukti visas vykdomas fonines užduotis ir palaukti, kol jos tinkamai išsivalys.
Pažangios sąvokos: žvilgsnis už
Derinimui ir introspekcijai `asyncio` suteikia keletą naudingų funkcijų:
asyncio.current_task()
: Grąžina `Task` objektą, skirtą šiuo metu vykdomam kodui.asyncio.all_tasks()
: Grąžina visų `Task` objektų, šiuo metu valdomų įvykių ciklo, rinkinį. Tai puikiai tinka derinimui, norint pamatyti, kas veikia.
Taip pat galite pridėti užbaigimo atgalinio ryšio funkcijas (angl. callbacks) prie užduočių naudodami `task.add_done_callback()`. Nors tai gali būti naudinga, dažnai tai lemia sudėtingesnę, atgalinio ryšio stiliaus kodo struktūrą. Šiuolaikiniai metodai, naudojantys `await`, `TaskGroup` ar `gather`, paprastai yra pageidaujami dėl aiškumo ir lengvo palaikymo.
Išvada
`asyncio.Task` yra lygiagretumo variklis šiuolaikiniame Python. Suprasdami, kaip kurti, valdyti ir grakščiai tvarkyti užduočių gyvavimo ciklą, galite paversti savo I/O ribotas programas iš lėtų, nuoseklių procesų į itin efektyvias, mastelį didinančias ir reaguojančias sistemas.
Apžvelgėme kelionę nuo pagrindinės korutinos planavimo koncepcijos su `create_task()` iki sudėtingų darbo eigų orkestravimo su `TaskGroup`, `gather()` ir `wait()`. Taip pat išnagrinėjome kritinę patikimo klaidų tvarkymo, atšaukimo ir laiko limitų svarbą kuriant atsparią programinę įrangą.
Asinchroninio programavimo pasaulis yra platus, tačiau užduočių įvaldymas yra pats svarbiausias žingsnis, kurį galite žengti. Pradėkite eksperimentuoti. Konvertuokite nuoseklią, I/O ribotą programos dalį, kad ji naudotų lygiagrečias užduotis, ir patys įsitikinkite našumo padidėjimu. Pasinaudokite lygiagretumo galia ir būsite puikiai pasirengę kurti naujos kartos didelio našumo Python programas.