Ачааллж байна...
Өмнөх хичээлд бид asyncio-гийн үндсийг тавьсан. Coroutine бол зогсож, үргэлжилж болох функц. Event loop тэднийг ээлжлэн ажиллуулдаг. Дараалсан await-ууд дараалсан хэвээр бөгөөд зэрэгцээ ажиллагаа loop-д олон coroutine-ийг нэг дор өгөхөд л бий болдог. Тэр "нэг дор өгөх" хэрэгслээр бид gather-ийг урьдчилан харсан. Энэ хичээл тэр хэрэгслүүдийг бүрэн эзэмшинэ.
Гурван хэрэгсэл бий. Task нь coroutine-ийг "цаана нь эхлүүл, үр дүнг дараа авна" гэж loop-д өгдөг: Хичээл 13.2-ын executor.submit ба Future-ийн asyncio хувилбар. TaskGroup нь олон task-ийг бүлэглэж, бүгд дуустал хүлээж, нэг нь бүтэлгүйтвэл бусдыг цэвэрхэн цуцалдаг: Python 3.11-ээс хойшхи хамгийн аюулгүй арга. timeout нь хэт удаан ажлыг хугацаагаар хязгаарлана.
Эдгээр гурвыг холбодог нэг ухагдахуун бий: цуцлалт, cancellation. Thread-ийг гаднаас нь найдвартай зогсоох арга Python-д байхгүй. Харин coroutine нь await-д зогсдог тул loop түүнийг тэр цэгт цуцалж чадна. Энэ хичээлд цуцлалт хэрхэн ажилладгийг trace-ээр харж, Хичээл 11.3-ын цэвэрлэгээний хэв маяг энд ч яг ижлээр хэрэгтэй болохыг олно. Мөн gather-ийн нэг аюултай зан төлөвийг хэмжинэ: алдаа гарахад бусад ажил "өнчин" болж үргэлжилдэг.
Барилгын ажлын туслан гүйцэтгэгчдийг төсөөлөөд үзээрэй. Ерөнхий гүйцэтгэгч цахилгаанчин, сантехникч, будагчинд ажил хуваарилна. Тус бүрд гэрээ өгнө: ажил явагдаж байна, дуусахад үр дүнг энэ гэрээгээр хүлээн авна. Гэрээ бол Task.
Одоо хоёр өөр ерөнхий гүйцэтгэгч. Эхнийх нь хайхрамжгүй: сантехникч "хоолой тохирохгүй, ажлыг хийх боломжгүй" гэж мэдэгдэхэд тэр үйлчлүүлэгчид шууд "төсөл бүтэлгүйтлээ" гэж хэлээд явчихна. Гэвч цахилгаанчин, будагчин хэн ч мэдэгдээгүй тул ажлаа үргэлжлүүлж, хэн ч хүлээж авахгүй хананд утас татаж, будаг шавсаар байна. Энэ бол gather-ийн анхдагч зан төлөв.
Хоёр дахь нь хариуцлагатай: сантехникчээс муу мэдээ ирмэгц бусад бүх туслан гүйцэтгэгч рүү утасдаж "ажлаа зогсоо, багажаа цэгцлээд гар" гэж хэлнэ. Тэд ажлаа зогсоож, цэвэрлээд гарсны дараа л тэр үйлчлүүлэгчид бүрэн тайлан өгнө: юу бүтэлгүйтсэн, яагаад. Энэ бол TaskGroup. "Ажлаа зогсоо" дуудлага бол цуцлалт. "Багажаа цэгцлээд гар" бол цуцлагдсан coroutine-ийн цэвэрлэгээ.
Мөн гэрээнд хугацаа заасан байж болно: "Баасан гараг гэхэд дуусаагүй бол ажил цуцлагдана". Энэ бол timeout.
Өмнөх хичээлийн fetch:
python
import asyncio
import time
async def fetch(name, seconds=1.0):
print(f"{name}: эхэллээ")
await asyncio.sleep(seconds)
print(f"{name}: дууслаа")
return f"{name}: татагдлаа"
async def main():
start = time.perf_counter()
task = asyncio.create_task(fetch("хуудас-1"))
print("main: task үүсгэлээ")
await asyncio.sleep(0.5)
print("main: хагас секунд өөр ажил хийлээ")
result = await task
print(result)
print(f"{time.perf_counter() - start:.2f} с")
asyncio.run(main())main: task үүсгэлээ
хуудас-1: эхэллээ
main: хагас секунд өөр ажил хийлээ
хуудас-1: дууслаа
хуудас-1: татагдлаа
1.00 сasyncio.create_task() coroutine-ийг Task болгон loop-д бүртгэнэ: "боломж гармагц үүнийг ажиллуул". Trace-ийг нарийн уншъя, учир нь эхний хоёр мөрийн дараалал олныг гайхуулдаг.
Алхам 1. create_task task-ийг бүртгэнэ, гэвч хараахан ажиллуулахгүй. main ганц thread-ийг эзэлж байгаа тул loop ээлж авах боломжгүй. “main: task үүсгэлээ” хэвлэгдэнэ.
Алхам 2. main await asyncio.sleep(0.5)-д хүрч ээлжээ өгнө. Loop одоо л task-ийг эхлүүлнэ: "хуудас-1: эхэллээ". Task өөрийн asyncio.sleep-д хүрч ээлжээ өгнө. Хоёулаа хүлээж байна.
Алхам 3. 0.5 секундэд main сэрнэ: "хагас секунд өөр ажил хийлээ". await task-д хүрнэ: task дуусаагүй тул main хүлээнэ.
Алхам 4. 1.0 секундэд task сэрж, "дууслаа" хэвлэж, return хийнэ. main сэрж, үр дүнг авна.
Нийт нэг секунд: main-ий хагас секундийн ажил ба task-ийн нэг секунд давхцсан. Task-ийн гол утга: coroutine-ийг шууд await хийвэл main түүнийг дуустал хүлээдэг. create_task хийвэл task цаана нь явж, main өөр ажил хийж, үр дүнг хүссэн үедээ авна. Хичээл 13.2-ын submit ба future.result()-тэй адил хэлбэр, ганц ялгаа нь thread биш, нэг loop дээрх coroutine.
Одоо алдаа. Бүтэлгүйтэж болох, мөн цуцлалтыг хэвлэдэг fetch:
python
async def flaky(name, seconds=1.0, fail=False):
try:
await asyncio.sleep(seconds)
except asyncio.CancelledError:
print(f"{name}: цуцлагдлаа")
raise
if fail:
raise ConnectionError(f"{name}: холболт тасарлаа")
print(f"{name}: дууслаа")
return f"{name}: татагдлаа"asyncio.CancelledError бол цуцлалтын дохио: loop coroutine-ийг цуцлахдаа түүний зогссон await-д энэ exception-ийг шиднэ. Хичээл 6.3-ын throw() механизмыг санаарай: generator-ийн зогссон yield-д exception шидэх. Яг ижил санаа. Бид түүнийг барьж, мессеж хэвлээд, ганц raise-ээр заавал дахин шиднэ: цуцлалтыг залгих нь Түгээмэл алдааны нэг. gather-ээр гурван хуудас, хоёр дахь нь хагас секундэд бүтэлгүйтэнэ:
python
async def main():
start = time.perf_counter()
try:
await asyncio.gather(
flaky("хуудас-1", 1.0),
flaky("хуудас-2", 0.5, fail=True),
flaky("хуудас-3", 2.0),
)
except ConnectionError as error:
print(f"{time.perf_counter() - start:.2f} с: алдаа барьлаа: {error}")
await asyncio.sleep(2.0)
print(f"{time.perf_counter() - start:.2f} с: main дууслаа")
asyncio.run(main())0.50 с: алдаа барьлаа: хуудас-2: холболт тасарлаа
хуудас-1: дууслаа
хуудас-3: дууслаа
2.50 с: main дууслааgather хуудас-2-ийн алдааг 0.5 секундэд шууд main руу дамжуулсан. main алдааг барьж, өөр ажилдаа шилжсэн. Гэвч хуудас-1 ба хуудас-3 цуцлагдаагүй: тэд цаана нь үргэлжилж, 1.0 ба 2.0 секундэд "дууслаа" хэвлэсэн. Тэдний үр дүнг хэн ч хүлээж аваагүй. Энэ бол хайхрамжгүй ерөнхий гүйцэтгэгч: өнчин ажлууд. Fake хуудас татахад хор хөнөөлгүй. Харин тэр ажлууд мэдээллийн санд бичдэг, төлбөр илгээдэг, эсвэл файл үүсгэдэг бол програм "бүтэлгүйтлээ" гэж мэдээлсний дараа тэд хийгдсээр байна.
gather-д өөр горим бий: return_exceptions=True. Тэр үед алдаа шидэгдэхгүй, үр дүнгийн жагсаалтад exception объект болон орно: ["хуудас-1: татагдлаа", ConnectionError(...), "хуудас-3: татагдлаа"]. Бүх ажил дуусах ба алдааг тус бүрээр нь шалгах нь Хичээл 13.2-ын as_completed загвартай адил. Гэвч алдаа гарахад бусдыг зогсоох хэрэгтэй бол gather-ийн аль ч горим тохирохгүй.
python
async def main():
start = time.perf_counter()
try:
async with asyncio.TaskGroup() as group:
task1 = group.create_task(flaky("хуудас-1", 1.0))
task2 = group.create_task(flaky("хуудас-2", 0.5, fail=True))
task3 = group.create_task(flaky("хуудас-3", 2.0))
except* ConnectionError as group_error:
for error in group_error.exceptions:
print(f"{time.perf_counter() - start:.2f} с: {error}")
await asyncio.sleep(2.0)
print(f"{time.perf_counter() - start:.2f} с: main дууслаа")
asyncio.run(main())хуудас-1: цуцлагдлаа
хуудас-3: цуцлагдлаа
0.50 с: хуудас-2: холболт тасарлаа
2.50 с: main дууслааasync with бол Бүлэг 6-ын with-ийн asyncio хувилбар: context manager-ийн орох ба гарах үйлдлүүд await хийж болно. TaskGroup-ийн гарах үйлдэл бүх task дуусахыг хүлээнэ: блок дотор үүсгэсэн task-уудыг блокоос гарахад бүгд дууссан байх баталгаатай. Өнчин ажил байх боломжгүй.
Trace. 0.5 секундэд хуудас-2 ConnectionError шиднэ. TaskGroup үүнийг мэдмэгц үлдсэн task-уудыг цуцална: хуудас-1 ба хуудас-3-ийн зогссон await-д CancelledError шидэгдэж, тэд "цуцлагдлаа" хэвлээд дахин шиднэ. Бүх task дууссаны, цуцлагдах ч дуусах гэсэн үг, дараа л TaskGroup алдааг main руу дамжуулна. Ажлууд 1.0 ба 2.0 секундэд "дууслаа" хэвлээгүй: тэд хэзээ ч дуусаагүй. Хариуцлагатай гүйцэтгэгч.
Хоёр шинэ синтакс бий. TaskGroup алдааг шууд биш, ExceptionGroup дотор боож шиддэг: олон task зэрэг бүтэлгүйтэж болох тул тэр нэг дор хэд хэдэн exception агуулж болно. except* бол ExceptionGroup-ийн тусгай except: бүлэг дотроос ConnectionError төрлийнхийг сонгож барина. group_error.exceptions тэдгээрийн tuple. Хэрэв бүлэгт өөр төрлийн exception, жишээлбэл ValueError, байсан бол except* ConnectionError түүнийг үлдээж, тэр нь дээш үргэлжилнэ: Хичээл 11.1-ийн "зөвхөн юу хийхээ мэддэгээ бар" дүрэм бүлэг хэлбэрээр. Хоёулаа Python 3.11-ээс хойш бий.
Амжилттай тохиолдолд үр дүнг task объектуудаас авна:
python
async def main():
async with asyncio.TaskGroup() as group:
tasks = [group.create_task(flaky(f"хуудас-{i}")) for i in range(3)]
print([task.result() for task in tasks])with блокийн дараа бүх task дууссан нь баталгаатай тул task.result() хүлээлтгүйгээр утга буцаана.
Зөвлөмж энгийн: шинэ кодонд олон task-ийг зэрэг ажиллуулахад TaskGroup анхдагч сонголт. gather-ийг бүх ажил алдаанаас үл хамааран дуусах ёстой, return_exceptions=True горимд, эсвэл хуучин кодтой ажиллахад хэрэглэ.
Сүлжээний хүсэлт заримдаа хэзээ ч хариу өгдөггүй: сервер гацсан, холболт чимээгүй тасарсан. Хязгааргүй хүлээх coroutine програмыг мөнхөд гацааж болно. asyncio.timeout() бол context manager: блок заасан хугацаанд дуусахгүй бол дотор нь буй ажлыг цуцалж, TimeoutError шиднэ:
python
async def main():
start = time.perf_counter()
try:
async with asyncio.timeout(1.5):
await flaky("удаан-хуудас", 3.0)
except TimeoutError:
print(f"{time.perf_counter() - start:.2f} с: 1.5 секундэд багтсангүй")
asyncio.run(main())удаан-хуудас: цуцлагдлаа
1.50 с: 1.5 секундэд багтсангүй1.5 секундэд timeout блок доторх ажлыг цуцалсан: flaky "цуцлагдлаа" хэвлэсэн. Дараа нь timeout CancelledError-ийг TimeoutError болгон хувиргаж шидсэн. TimeoutError бол Python-ий стандарт exception, Хичээл 11.2-ын PaymentTimeoutError-ийн жишээнд бид түүнтэй танилцсан.
timeout бол цуцлалтын механизм дээр бүтээгдсэн тул хоёр хэрэгсэл хамт сайхан ажилладаг. Task бүрд тусдаа хязгаар өгөх бол туслах coroutine бичнэ:
python
async def fetch_limited(name, seconds, limit):
try:
async with asyncio.timeout(limit):
return await flaky(name, seconds)
except TimeoutError:
return f"{name}: хугацаа хэтэрлээ"
async def main():
async with asyncio.TaskGroup() as group:
tasks = [
group.create_task(fetch_limited("хурдан", 0.5, limit=1.0)),
group.create_task(fetch_limited("удаан", 3.0, limit=1.0)),
]
print([task.result() for task in tasks])
asyncio.run(main())хурдан: дууслаа
удаан: цуцлагдлаа
['хурдан: татагдлаа', 'удаан: хугацаа хэтэрлээ']Удаан хуудас өөрийн хязгаартаа хүрч, TimeoutError-оо өөрөө барьж, тодорхой утга буцаасан. TaskGroup-ийн үүднээс тэр амжилттай дууссан тул бусад task цуцлагдаагүй. Энэ бол Хичээл 12.4-ийн импортлогчийн загвар: нэг мөрийн асуудал бусдыг зогсоохгүй. Хуучин кодонд ижил ажлыг хийдэг asyncio.wait_for(coro, timeout) функцтэй таарч болно.
Цуцлалт бол await-д шидэгддэг exception тул Хичээл 11.3-ын бүх цэвэрлэгээний хэрэгсэл энд ч ажиллана. Хэрэв coroutine түр файл үүсгэж, холболт нээж, эсвэл түгжээ барьж байгаад цуцлагдвал finally эсвэл with тэр нөөцийг суллана:
python
async def download_to_file(name):
temp_name = f"{name}.part"
try:
with open(temp_name, "w", encoding="utf-8") as f:
await asyncio.sleep(2.0)
f.write("агуулга")
finally:
print(f"{name}: цэвэрлэгээ хийлээ")Энэ coroutine-ийг timeout-аар цуцлахад finally ажиллаж, with файлыг хаана. Хичээл 11.3-ын atomic_write-тэй хослуулбал цуцлагдсан татаж авалт хагас файл үлдээхгүй. Цуцлалт ердийн exception шиг ажилладаг нь asyncio-гийн хамгийн гоё шийдвэрүүдийн нэг: шинэ цэвэрлэгээний механизм сурах шаардлагагүй.
Хүсвэл TaskGroup жишээнд хоёр task-ийг зэрэг бүтэлгүйтүүлж, нэгийг ConnectionError-оор, нөгөөг ValueError-оор, except* хоёр блок бичиж, ExceptionGroup хоёр төрлийг хэрхэн хуваадгийг харж үзээрэй. Сонирхвол Хичээл 5.3-ын retry decorator-ийн async хувилбар бичиж болно: wrapper async def болж, time.sleep-ийн оронд await asyncio.sleep. Мөн download_to_file-ийг timeout-аар цуцалж, .part файл үлдсэн эсэхийг шалгаад, atomic_write-ийн санаагаар засах нь энэ хичээлийн ба Хичээл 11.3-ын холбоог бүрэн харуулна. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
Task exception was never retrieved. “Task-ийн exception хэзээ ч авагдсангүй” гэсэн утгатай, араас нь future: <Task finished name='Task-2' coro=<flaky() ...> exception=ConnectionError(...)> гэсэн мөр гарна. create_task хийсэн task алдаа шидсэн ч хэн ч түүнийг await хийгээгүй эсвэл result() дуудаагүй үед loop үүнийг log-д бичнэ. Энэ бол Хичээл 13.2-ын "алдаа чимээгүй алга болох"-ын asyncio хувилбар, гэхдээ asyncio ядаж анхааруулдаг. Шийдэл: task бүрийг await хий, эсвэл TaskGroup хэрэглэ: TaskGroup бүх task-ийн үр дүнг өөрөө хүлээнэ.
Task чимээгүй алга болох. Мессежгүй. asyncio.create_task(fetch(...))-ийн үр дүнг хувьсагчид хадгалаагүй бол loop тэр task-ийг зөвхөн сул лавлагаагаар барьдаг тул Python-ий санах ойн цэвэрлэгч түүнийг ажлын дунд устгаж болно. asyncio-гийн албан ёсны баримт бичиг энэ талаар тусгайлан сэрэмжлүүлдэг. "Илгээгээд мартах" task хэрэгтэй бол тэдгээрийг set-д хадгал: background.add(task) ба task.add_done_callback(background.discard). Эсвэл TaskGroup: тэр task-уудаа өөрөө барина.
Цуцлалтыг залгих. Мессежгүй, ноцтой. except asyncio.CancelledError: pass эсвэл өргөн except BaseException: нь цуцлалтыг идэж, coroutine үргэлжлэн ажиллана. timeout ба TaskGroup цуцлалт дээр тулгуурладаг тул тэд эвдэрнэ: timeout хугацаагаа хэтрүүлж, TaskGroup дуусахгүй хүлээж болно. CancelledError-ийг барьсан бол цэвэрлэгээгээ хийгээд заавал дахин шид, Хичээл 11.3-ын BaseException дүрэмтэй ижил.
except ConnectionError ExceptionGroup-ийг барихгүй. Мессеж нь traceback-д ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception) гэж харагдана: “TaskGroup-д барьагдаагүй алдаа (1 дэд exception)”. TaskGroup алдааг боож шиддэг тул ердийн except ConnectionError тохирохгүй, алдаа дээш үргэлжилнэ. except* хэрэглэ. Мөн нэг try-д except ба except* хоёрыг холих боломжгүй: SyntaxError: cannot have both 'except' and 'except*' on the same 'try'.
gather алдаа гарахад бусдыг зогсооно гэж бодох. Мессежгүй, хэмжилтээр харсан. Анхдагч gather алдааг шууд дамжуулдаг, гэвч бусад ажлыг цуцалдаггүй: тэд өнчин үргэлжилнэ. Алдаа гарахад бүгдийг зогсоох хэрэгтэй бол TaskGroup.
asyncio.create_task() coroutine-ийг Task болгон loop-д бүртгэнэ: тэр main ээлжээ өгмөгц эхэлж, цаана нь явна, үр дүнг await task-аар авна. Task-ийн лавлагааг хадгалах ёстой, эс бөгөөс тэр алга болж болно. Цуцлалт бол coroutine-ийн зогссон await-д шидэгддэг CancelledError: барьж цэвэрлэж болно, гэвч заавал дахин шидэх ёстой. finally ба with цуцлагдсан coroutine-ийн нөөцийг ердийн exception-ий адил суллана. Анхдагч gather алдааг шууд дамжуулдаг, гэвч бусад ажлыг цуцалдаггүй тул тэд өнчин үргэлжилнэ. return_exceptions=True бүх ажлыг дуусгаж, алдааг үр дүнгийн жагсаалтад өгнө. TaskGroup бол Python 3.11-ээс хойшхи анхдагч сонголт: async with блок бүх task дуусахыг хүлээнэ, нэг нь бүтэлгүйтвэл бусдыг цуцалж, алдааг ExceptionGroup-д боож шиднэ, except* түүнийг төрлөөр нь задалж барина. asyncio.timeout() блокийг хугацаагаар хязгаарлаж, хэтэрвэл цуцлаад TimeoutError шиднэ. Task бүрд тусдаа хязгаарыг туслах coroutine-оор өгнө.
Одоо бид мянган task-ийг хэдхэн мөрөөр эхлүүлж чадна. Гэвч Хичээл 13.2-т хэлсэнчлэн бодит сервер рүү мянган хүсэлт зэрэг илгээх нь эелдэг биш бөгөөд "хэт олон хүсэлт" гэсэн татгалзал авна. Мөн Хичээл 14.1-ийн хамгийн чухал дүрэм, loop-ийг бүү блокло, бодит ажилд нэг асуулт үүсгэдэг: async хувилбаргүй хуучин сан эсвэл урт тооцоолол хийх зайлшгүй бол яах вэ? Дараагийн хичээл эдгээр хоёр бодит асуудлын хэрэгслүүдийг судална: зэрэг ажлын тоог хязгаарлах Semaphore, блоклох ажлыг thread руу шилжүүлэх asyncio.to_thread.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.