Atklājiet Python paralēlās programmēšanas jaudu. Uzziniet, kā izveidot, pārvaldīt un atcelt asyncio uzdevumus, lai veidotu augstas veiktspējas, mērogojamas lietojumprogrammas.
Apgūstiet Python asyncio: Iepazīstiet uzdevumu izveidi un pārvaldību
Mūsdienu programmatūras izstrādes pasaulē veiktspēja ir vissvarīgākā. Lietojumprogrammām ir jābūt atsaucīgām, apstrādājot tūkstošiem vienlaicīgu tīkla savienojumu, datubāzes vaicājumu un API izsaukumu, neizraisot problēmas. I/O saistošajām operācijām—kur programma lielāko daļu laika pavada, gaidot ārējos resursus, piemēram, tīklu vai disku—tradicionālais sinhronais kods var kļūt par ievērojamu šķērsli. Tieši šeit izceļas asinhronā programmēšana, un Python bibliotēka asyncio
ir atslēga šīs jaudas atklāšanai.
asyncio
paralēlismam centrā ir vienkāršs, bet spēcīgs jēdziens: Uzdevums (Task). Kamēr koprogrāmas definē, ko darīt, Uzdevumi ir tie, kas faktiski paveic darbu. Tie ir vienlaicīgas izpildes pamatvienība, ļaujot jūsu Python programmām vienlaicīgi veikt vairākas darbības, ievērojami uzlabojot caurlaides spēju un atsaucību.
Šī visaptverošā rokasgrāmata sniegs jums padziļinātu ieskatu asyncio.Task
. Mēs izskatīsim visu, sākot no pamatiem līdz izveidei, līdz progresīvākām pārvaldības metodēm, atcelšanai un labākajām praksēm. Neatkarīgi no tā, vai veidojat augstas trafika tīmekļa pakalpojumu, datu skrāpēšanas rīku vai reāllaika lietojumprogrammu, Uzdevumu apgūšana ir būtiska prasme jebkuram modernam Python izstrādātājam.
Kas ir koprogramma? Ātrs atkārtojums
Pirms mēs varam skriet, mums ir jāsāk staigāt. Un asyncio
pasaulē staigāšana ir koprogrammu izpratne. Koprogramma ir īpašs funkcijas veids, kas definēts ar async def
.
Kad izsaucat parastu Python funkciju, tā tiek izpildīta no sākuma līdz beigām. Tomēr, kad izsaucat koprogrammas funkciju, tā netiek izpildīta nekavējoties. Tā vietā tā atgriež koprogrammas objektu. Šis objekts ir darba plāns, kas jāizdara, bet pats par sevi tas ir neaktīvs. Tā ir apturēta aprēķināšana, ko var sākt, apturēt un atsākt.
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)
Burvju atslēgvārds ir await
. Tas notiek notikumu ciklam: "Šī operācija var aizņemt ilgu laiku, tāpēc droši mani šeit apturiet un dodieties strādāt pie kaut kā cita. Pamodiniet mani, kad šī operācija būs pabeigta." Šī spēja apturēt un pārslēgt kontekstus ir tas, kas nodrošina paralēlismu.
Paralēlisma sirds: asyncio.Task izpratne
Tātad, koprogramma ir plāns. Kā mēs sakām virtuvei (notikumu ciklam), lai tā sāk gatavot? Šeit parādās asyncio.Task
.
asyncio.Task
ir objekts, kas iesaiņo koprogrammu un ieplāno to izpildei asyncio notikumu ciklā. Domājiet par to šādi:
- Koprogramma (
async def
): Detalizēta ēdiena recepte. - Notikumu cikls: Centrālais virtuves, kur notiek visa gatavošana.
await my_coro()
: Jūs stāvat virtuvē un paši sekojat receptei soli pa solim. Jūs nevarat darīt neko citu, kamēr ēdiens nav gatavs. Tas ir secīgs izpilde.asyncio.create_task(my_coro())
: Jūs nododat recepti pavāram (Uzdevumam) virtuvē un sakāt: "Sāc ar šo strādāt." Pavārs sāk nekavējoties, un jūs varat darīt citas lietas, piemēram, izdalīt vairāk recepšu. Tas ir paralēls izpilde.
Galvenā atšķirība ir tā, ka asyncio.create_task()
ieplāno koprogrammu darboties "fonā" un nekavējoties atdod kontroli jūsu kodam. Jūs saņemat atpakaļ Task
objektu, kas darbojas kā saite uz šo notiekošo operāciju. Jūs varat izmantot šo saiti, lai pārbaudītu tā statusu, atceltu to vai vēlāk gaidītu tā rezultātu.
Pirmie uzdevumi: `asyncio.create_task()` funkcija
Galvenais veids, kā izveidot Uzdevumu, ir ar asyncio.create_task()
funkciju. Tā pieņem koprogrammas objektu kā argumentu un ieplāno to izpildei.
Pamata sintakse
Lietošana ir vienkārša:
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())
Ievērojiet, kā izvade parāda, ka `main` funkcija turpina izpildi nekavējoties pēc uzdevuma izveidošanas. Tā nebloķē. Tā apturās tikai tad, kad beigās skaidri await task
.
Praktisks piemērs: vienlaicīgi tīmekļa pieprasījumi
Apskatīsim patieso Uzdevumu jaudu ar izplatītu scenāriju: datu izgūšana no vairākām URL. Tam izmantosim populāro bibliotēku `aiohttp`, kuru var instalēt ar `pip install aiohttp`.
Vispirms apskatīsim secīgo (lēno) veidu:
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())
Ja katrs pieprasījums aizņem apmēram 0.5 sekundes, kopējais laiks būs aptuveni 2 sekundes, jo katrs `await` bloķē ciklu, līdz tas viens pieprasījums ir pabeigts.
Tagad atbrīvosim paralēlisma jaudu ar Uzdevumiem:
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())
Kad palaidīsit paralēlo versiju, redzēsiet dramatisku atšķirību. Kopējais laiks būs aptuveni garākais atsevišķais pieprasījums, nevis visu pieprasījumu summa. Tas ir tāpēc, ka, tiklīdz pirmā `fetch_status` koprogramma sasniedz savu `await session.get(url)`, notikumu cikls to aptur un nekavējoties sāk nākamo. Visi tīkla pieprasījumi notiek efektīvi vienlaicīgi.
Uzdevumu grupas pārvaldīšana: svarīgas metodes
Atsevišķu uzdevumu izveide ir lieliska, bet reālos lietojumos jums bieži vien ir jāpalaiž, jāpārvalda un jāsinhronizē visa to grupa. `asyncio` nodrošina vairākus jaudīgus rīkus tam.
Modernā pieeja (Python 3.11+): `asyncio.TaskGroup`
Ieviestā Python 3.11 versijā, `TaskGroup` ir jaunais, ieteicamais un drošākais veids, kā pārvaldīt saistītu uzdevumu grupu. Tā nodrošina tā saukto strukturēto paralēlismu.
Galvenās `TaskGroup` funkcijas:
- Garantēta tīrīšana: `async with` bloks neizies, kamēr visi tajā izveidotie uzdevumi nebūs pabeigti.
- Izturīga kļūdu apstrāde: Ja kāds uzdevums grupā izraisīs kļūdu, visi citi grupas uzdevumi tiks automātiski atcelti, un kļūda (vai `ExceptionGroup`) tiks atkārtoti izraisīta, izejot no `async with` bloka. Tas novērš pamestus uzdevumus un nodrošina paredzamu stāvokli.
Lūk, kā to izmantot:
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())
Kad palaidīsit šo, redzēsiet, ka `worker(2)` izsauc kļūdu. `TaskGroup` to uztver, atceļ citus darbināmos uzdevumus (kā `worker(3)`), un pēc tam izsauc `ExceptionGroup`, kas satur `ValueError`. Šī metode ir neticami izturīga, lai veidotu uzticamas sistēmas.
Klasiskais darba zirgs: `asyncio.gather()`
Pirms `TaskGroup`, `asyncio.gather()` bija visizplatītākais veids, kā vienlaicīgi palaist vairākas gaidītās lietas un gaidīt, kamēr tās visas beidzas.
`gather()` pieņem koprogrammu vai Uzdevumu secību, izpilda tās visas un atgriež to rezultātu sarakstu tādā pašā secībā kā ievadītie dati. Tā ir augsta līmeņa, ērta funkcija biežākajam gadījumam "palaižam visas šīs lietas un iedodiet man visus rezultātus".
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())
Kļūdu apstrāde ar `gather()`: Pēc noklusējuma, ja kāda no `gather()` pieņemtajām gaidāmajām lietām izsauc kļūdu, `gather()` nekavējoties izplata šo kļūdu, un citi darbināmie uzdevumi tiek atcelti. Jūs varat mainīt šo uzvedību ar `return_exceptions=True`. Šajā režīmā, tā vietā, lai izsauktu kļūdu, tā tiks ievietota rezultātu sarakstā atbilstošajā pozīcijā.
# ... 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)
Smalkgraudaina kontrole: `asyncio.wait()`
asyncio.wait()` ir zemāka līmeņa funkcija, kas piedāvā detalizētāku kontroli pār uzdevumu grupu. Atšķirībā no `gather()`, tā neatgriež rezultātus tieši. Tā vietā tā atgriež divus uzdevumu kopumus: `done` un `pending`.
Tās visspēcīgākā funkcija ir `return_when` parametrs, kas var būt:
asyncio.ALL_COMPLETED
(noklusējums): Atgriež, kad visi uzdevumi ir pabeigti.asyncio.FIRST_COMPLETED
: Atgriež, tiklīdz vismaz viens uzdevums ir pabeigts.asyncio.FIRST_EXCEPTION
: Atgriež, kad uzdevums izsauc kļūdu. Ja neviens uzdevums neizsauc kļūdu, tas ir ekvivalentsALL_COMPLETED
.
Tas ir ārkārtīgi noderīgi scenārijiem, piemēram, vaicājot vairākus liekus datu avotus un izmantojot pirmo, kas atbild:
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(): Kad ko lietot?
- Lietojiet `asyncio.TaskGroup` (Python 3.11+) kā savu noklusējuma izvēli. Tā strukturētās paralēlisma metode ir drošāka, sakārtotāka un mazāk pakļauta kļūdām uzdevumu grupas pārvaldīšanai, kas pieder vienai loģiskai operācijai.
- Lietojiet `asyncio.gather()`, kad jums ir jāpalaiž neatkarīgu uzdevumu grupa un vienkārši vēlaties savu rezultātu sarakstu. Tā joprojām ir ļoti noderīga un nedaudz kodolīgāka vienkāršiem gadījumiem, īpaši Python versijās pirms 3.11.
- Lietojiet `asyncio.wait()` progresīviem scenārijiem, kur nepieciešama smalkgraudaina kontrole pār pabeigšanas nosacījumiem (piemēram, gaidīt pirmo rezultātu) un esat gatavi manuāli pārvaldīt atlikušos gaidītos uzdevumus.
Uzdevumu dzīves cikls un pārvaldība
Kad Uzdevums ir izveidots, jūs varat ar to mijiedarboties, izmantojot metodes uz Task
objekta.
Uzdevumu statusa pārbaude
task.done()
: AtgriežTrue
, ja uzdevums ir pabeigts ( veiksmīgi, ar kļūdu vai atcelts).task.cancelled()
: AtgriežTrue
, ja uzdevums tika atcelts.task.exception()
: Ja uzdevums izraisīja kļūdu, šī metode atgriež kļūdas objektu. Pretējā gadījumā tā atgriežNone
. Jūs varat izsaukt šo metodi tikai pēc tam, kad uzdevums irdone()
.
Rezultātu iegūšana
Galvenais veids, kā iegūt uzdevuma rezultātu, ir vienkārši `await task`. Ja uzdevums beidzās veiksmīgi, tas atgriež vērtību. Ja tas izraisīja kļūdu, `await task` atkārtoti izraisīs šo kļūdu. Ja tas tika atcelts, `await task` izraisīs `CancelledError`.
Alternatīvi, ja zināt, ka uzdevums ir `done()`, varat izsaukt `task.result()`. Tas darbojas identiski `await task` attiecībā uz vērtību atgriešanu vai kļūdu izraisīšanu.
Atcelšanas māksla
Spēja pieklājīgi atcelt ilgi darbojošās operācijas ir ļoti svarīga, lai veidotu izturīgas lietojumprogrammas. Jums var būt nepieciešams atcelt uzdevumu sakarā ar taimautu, lietotāja pieprasījumu vai kļūdu citur sistēmā.
Jūs atceļat uzdevumu, izsaucot tā metodi task.cancel()
. Tomēr tas neaptur uzdevumu nekavējoties. Tā vietā tā ieplāno, ka CancelledError
izņēmums tiks izmests koprogrammas iekšienē nākamajā await
punktā. Šī ir būtiska detaļa. Tā dod koprogrammai iespēju sakārtot lietas pirms iziešanas.
Labi uzvedošai koprogrammai vajadzētu veiksmīgi apstrādāt šo `CancelledError`, parasti izmantojot `try...finally` bloku, lai nodrošinātu, ka tādi resursi kā failu turētāji vai datubāzes savienojumi tiek aizvērti.
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
bloks ir garantēts, ka tiks izpildīts, padarot to par ideālu vietu tīrīšanas loģikai.
Laika ierobežojumu pievienošana ar `asyncio.timeout()` un `asyncio.wait_for()`
Manuāla pauze un atcelšana ir apgrūtinoša. `asyncio` nodrošina palīglīdzekļus šai izplatītajai metodei.
Python 3.11+ versijā `asyncio.timeout()` konteksta pārvaldnieks ir ieteicamā metode:
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())
Vecākām Python versijām varat izmantot `asyncio.wait_for()`. Tas darbojas līdzīgi, bet iesaiņo gaidāmo lietu funkcijas izsaukumā:
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())
Abi rīki darbojas, atceļot iekšējo uzdevumu, kad taimauts ir sasniegts, izraisot `TimeoutError` (kas ir `CancelledError` apakšklase).
Izplatītas kļūdas un labākās prakses
Darbs ar Uzdevumiem ir spēcīgs, taču ir daži izplatīti slazdi, no kuriem jāizvairās.
- Kļūda: "Palaid un aizmirsti" kļūda. Uzdevuma izveide ar `create_task` un pēc tam tā nekad netiek gaidīta (vai tāda pārvaldnieka kā `TaskGroup`) ir bīstama. Ja šis uzdevums izsauc kļūdu, kļūda var tikt klusi zaudēta, un jūsu programma var izbeigties pirms uzdevums pat pabeidz savu darbu. Vienmēr nodrošiniet, lai katram uzdevumam būtu skaidrs īpašnieks, kas ir atbildīgs par tā rezultāta gaidīšanu.
- Kļūda: `asyncio.run()` sajaukšana ar `create_task()`. `asyncio.run(my_coro())` ir galvenais punkts, lai sāktu `asyncio` programmu. Tā izveido jaunu notikumu ciklu un palaida doto koprogrammu, līdz tā tiek pabeigta. `asyncio.create_task(my_coro())` tiek izmantots *kaut kur* jau darbināmā asinhronā funkcijā, lai ieplānotu paralēlu izpildi.
- Labākā prakse: Lietojiet `TaskGroup` modernam Python. Tā dizains novērš daudzas izplatītas kļūdas, piemēram, aizmirstus uzdevumus un neapstrādātas kļūdas. Ja izmantojat Python 3.11 vai jaunāku versiju, padariet to par savu noklusējuma izvēli.
- Labākā prakse: Nosauciet savus uzdevumus. Izveidojot uzdevumu, izmantojiet parametru
name
:asyncio.create_task(my_coro(), name='DataProcessor-123')
. Tas ir nenovērtējams atkļūdošanai. Kad uzskaitāt visus darbināmos uzdevumus, jēgpilni nosaukumi palīdz jums saprast, ko jūsu programma dara. - Labākā prakse: Nodrošiniet pieklājīgu izslēgšanos. Kad jūsu lietojumprogrammai ir jāizslēdzas, pārliecinieties, ka jums ir mehānisms, lai atceltu visus darbināmos fona uzdevumus un gaidītu, kamēr tie pienācīgi sakārtojas.
Progresīvāki jēdzieni: ieskats tālāk
Atkļūdošanai un introspekcijai `asyncio` nodrošina dažas noderīgas funkcijas:
asyncio.current_task()
: AtgriežTask
objektu kodam, kas pašlaik tiek izpildīts.asyncio.all_tasks()
: Atgriež visu pašlaik notikumu ciklā pārvaldītoTask
objektu kopu. Tas ir lieliski piemērots atkļūdošanai, lai redzētu, kas darbojas.
Jūs varat arī pievienot pabeigšanas atzvanus uzdevumiem, izmantojot task.add_done_callback()
. Lai gan tas var būt noderīgs, tas bieži noved pie sarežģītākas, atzvanu stila kodēšanas struktūras. Mūsdienu metodes, izmantojot await
, TaskGroup
vai gather
, parasti ir vēlams lasāmībai un uzturēšanai.
Secinājums
asyncio.Task
ir mūsdienu Python paralēlisma dzinējspēks. Izprotot, kā izveidot, pārvaldīt un pieklājīgi apstrādāt uzdevumu dzīves ciklu, jūs varat pārvērst savas I/O saistošās lietojumprogrammas no lēniem, secīgiem procesiem par ļoti efektīvām, mērogojamām un atsaucīgām sistēmām.
Mēs esam izskatījuši ceļu no pamata koncepcijas ieplānot koprogrammu ar `create_task()` līdz sarežģītu darbplūsmu orchestrēšanai ar `TaskGroup`, `gather()` un `wait()`. Mēs arī izskatījām kritisko nozīmi izturīgai kļūdu apstrādei, atcelšanai un taimautiem, lai veidotu izturīgu programmatūru.
Asinhronās programmatūras pasaule ir plaša, taču Uzdevumu apgūšana ir nozīmīgākais solis, ko varat spert. Sāciet eksperimentēt. Konvertējiet secīgu, I/O saistošu daļu savas lietojumprogrammas, lai izmantotu paralēlus uzdevumus, un vērojiet veiktspējas pieaugumu paši. Aptveriet paralēlisma jaudu, un jūs būsiet labi aprīkoti, lai veidotu nākamās paaudzes augstas veiktspējas Python lietojumprogrammas.