Ачааллж байна...
Өмнөх бүлэгт бид хүлээлтийг thread-ээр давхцуулсан. Thread ажилласан, гэвч хоёр үнэтэй байсан. Нэгдүгээрт, thread бүр үйлдлийн системийн бодит нөөц: санах ой эзэлдэг, мянга мянгаар нь үүсгэхэд систем хүндэрнэ. Хоёрдугаарт, thread-үүдийн хооронд хэзээ солихыг үйлдлийн систем шийддэг, аль ч мөчид, тэр ч байтугай нэг мөр кодын дунд. Хичээл 13.2-ын race condition яг тэр шийдвэрээс үүссэн: бид солилтыг хянадаггүй байсан.
asyncio өөр замаар явдаг. Ганц thread, ганц гүйцэтгэгч. Гэхдээ код өөрөө "би энд хүлээх гэж байна, энэ хооронд өөр ажил хийж болно" гэдгийг тодорхой зарладаг. Солилт зөвхөн тэр зарласан цэгүүдэд л явагдана. Ийм загварыг cooperative multitasking, хамтын олон ажиллагаа гэдэг: ажлууд ээлжээ сайн дураараа өгдөг. Thread-ийн загварыг харин preemptive, "булаан авах" гэдэг: үйлдлийн систем ээлжийг хүчээр авдаг.
Энэ хичээлд бид asyncio-гийн хоёр үндсэн ухагдахууныг эзэмшинэ: coroutine ба event loop. Эхлээд тэдгээрийг Бүлэг 2-ын generator-оос бүтээх бөгөөд хорин мөр кодоор жижиг event loop бичнэ. Энэ бол asyncio-гийн зүрх бөгөөд таны аль хэдийн мэдэх санаа дээр тогтдог. Дараа нь жинхэнэ asyncio-д шилжиж, async def ба await-ийг хэрэглэнэ. Гайхмаар үр дүнг хэмжинэ: async бичсэн код автоматаар зэрэг ажилладаггүй. Төгсгөлд нь asyncio-гийн хамгийн чухал дүрмийг хэмжилтээр харна: event loop-ийг хэзээ ч бүү блокло.
Шатрын их мастерын симултан тоглолтыг төсөөлөөд үзээрэй. Танхимд хорин ширээ, хорин өрсөлдөгч. Мастер ганцаараа. Тэр нэг ширээний өмнө зогсож нүүдлээ хийгээд, өрсөлдөгчийн бодохыг хүлээлгүй дараагийн ширээ рүү явна. Хорин ширээг тойроод эхний ширээ рүү буцаж ирэхэд өрсөлдөгч аль хэдийн нүүсэн байна. Мастер нэг агшинд зөвхөн нэг ширээнд нүүдэл хийдэг, хоёр гараараа хоёр ширээнд зэрэг нүүдэг биш. Гэвч хорин тоглолт бүгд урагшилж байна, учир нь тоглолтын хугацааны ихэнх нь өрсөлдөгчийн бодох хугацаа: мастерт хүлээх шаардлагагүй хугацаа.
Хоёр чухал дүрэм бий. Нэгдүгээрт, мастер ширээнээс зөвхөн нүүдлээ дуусгасны дараа л холдоно, нүүдлийн дунд хэзээ ч биш. Тиймээс нэг ширээний байдал хэзээ ч хагас өөрчлөгдөхгүй. Хоёрдугаарт, хэрэв мастер нэг ширээнд арван минут бодож зогсвол бусад арван есөн ширээ бүгд хүлээнэ: тэр хооронд хэн ч урагшлахгүй.
asyncio-д мастер бол event loop: ажлуудыг ээлжлэн ажиллуулдаг давталт. Ширээ бүр бол coroutine: зогсож, дараа нь үргэлжилж болох ажил. Нүүдлээ дуусгаад холдох мөч бол await: "би одоо хүлээх гэж байна". Өрсөлдөгчийн бодох хугацаа бол сүлжээ, диск гэх мэт I/O. Арван минут бодож зогсох мастер бол хоёр дахь дүрмийн сануулга: event loop-ийг блоклох нь бүх ажлыг зогсооно.
Хичээл 2.1-ийг санаарай: generator бол зогсоох товчтой функц. yield-д хүрмэгц зогсож, бүх дотоод төлөвөө хадгалж, next() дуудагдахад яг тэр газраасаа үргэлжилнэ. Энэ бол симултан тоглолтын ширээ: зогсож, хүлээж, буцаж ирэхэд үргэлжилдэг ажил.
python
from collections import deque
def task(name, steps):
for step in range(1, steps + 1):
print(f"{name}: алхам {step}")
yield
print(f"{name}: дууслаа")
def run_all(tasks):
queue = deque(tasks)
while queue:
current = queue.popleft()
try:
next(current)
except StopIteration:
continue
queue.append(current)
run_all([task("A", 3), task("B", 2), task("C", 1)])A: алхам 1
B: алхам 1
C: алхам 1
A: алхам 2
B: алхам 2
C: дууслаа
A: алхам 3
B: дууслаа
A: дууслааrun_all бол бидний event loop. Хичээл 12.1-ийн deque ажлуудын дараалал болно. Давталт бүрт дарааллын эхнээс нэг ажил авч, next()-ээр дараагийн yield хүртэл ажиллуулна: мастерын нэг нүүдэл. Хэрэв ажил StopIteration шидвэл, Хичээл 1.3-ын дохио, тэр дууссан тул буцааж нэмэхгүй. Үгүй бол дарааллын төгсгөлд нэмж, дараагийн ажил руу шилжинэ.
Trace-ийг уншъя. A эхний алхмаа хийгээд yield-д зогсоно. Loop B-ийг авна, B эхний алхмаа хийнэ. C эхний алхмаа хийнэ. Буцаж A руу. Гурван ажил ээлжилж байна, гэвч нэг агшинд зөвхөн нэг нь ажилладаг: мастер ганцаараа. Хэн ч өөрийг нь хүчээр зогсоогоогүй, ажил бүр yield-ээр ээлжээ сайн дураараа өгсөн. Энэ бол cooperative multitasking бүрэн хэлбэрээр.
Бидний жижиг loop нэг зүйл дутуу: ажил юу хүлээж байгааг мэддэггүй, бүгдийг хэлбэлзэлгүй ээлжилнэ. Жинхэнэ asyncio-гийн event loop мэддэг: "энэ ажил сүлжээний хариуг хүлээж байна, хариу ирэх хүртэл түүнийг бүү сэрээ", "энэ ажил нэг секунд унтаж байна, нэг секундийн дараа сэрээ". Энэ мэдлэгийг үйлдлийн системээс авдаг: систем аль сүлжээний холболтод өгөгдөл ирснийг мэдэгддэг. Бусад бүх зүйл, ээлж, зогсолт, үргэлжлэл, бидний хорин мөртэй ижил санаа.
Жинхэнэ asyncio-д generator-ийн yield-ийн оронд тусгай синтакс бий:
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 нь coroutine function тодорхойлно: зогсож, үргэлжилж болох функц. await бол зогсох цэг: "энэ үйлдэл дуусах хүртэл намайг зогсоо, тэр хооронд event loop өөр ажил хийж болно". asyncio.sleep() бол Хичээл 13.1-ийн time.sleep()-ийн asyncio хувилбар: заасан хугацаанд хүлээнэ, гэвч loop-ийг блоклохгүй, ээлжээ өгнө. Энэ хоёрын ялгаа хичээлийн төгсгөлд хэмжигдэнэ.
Хамгийн түрүүнд гайхуулдаг зүйл: coroutine function-ийг дуудахад функц ажиллахгүй.
python
result = fetch("хуудас-1")
print(result)<coroutine object fetch at 0x000001F3A2B4C5E0>
RuntimeWarning: coroutine 'fetch' was never awaited"эхэллээ" хэвлэгдээгүй. fetch("хуудас-1") нь coroutine object буцаасан: Хичээл 2.1-т generator function дуудахад generator объект буцаадагтай яг ижил. Ширээ тавигдсан, гэвч тоглолт эхлээгүй. Python програм дуусахад хэзээ ч ажиллаагүй coroutine-ийг илрүүлж анхааруулга гаргасан: “coroutine ‘fetch’ хэзээ ч await хийгдсэнгүй”.
Coroutine-ийг ажиллуулахын тулд event loop хэрэгтэй. asyncio.run() нь loop үүсгэж, өгсөн coroutine-ийг дуустал ажиллуулж, loop-ийг хаана:
python
async def main():
start = time.perf_counter()
first = await fetch("хуудас-1")
second = await fetch("хуудас-2")
third = await fetch("хуудас-3")
print(first, second, third, sep="\n")
print(f"{time.perf_counter() - start:.2f} с")
asyncio.run(main())хуудас-1: эхэллээ
хуудас-1: дууслаа
хуудас-2: эхэллээ
хуудас-2: дууслаа
хуудас-3: эхэллээ
хуудас-3: дууслаа
хуудас-1: татагдлаа
хуудас-2: татагдлаа
хуудас-3: татагдлаа
3.00 сmain бол програмын coroutine-уудын эхлэх цэг: asyncio.run түүнийг ажиллуулна. await нь coroutine-ийг ажиллуулж, дуустал хүлээж, return-ийн утгыг буцаана: first хувьсагч "хуудас-1: татагдлаа" string-ийг авсан.
Одоо хугацааг хараарай: 3.00 секунд. Async бичсэн, await хэрэглэсэн, гэвч хурдлаагүй. Энэ бол asyncio-гийн хамгийн түгээмэл буруу ойлголт: async def ба await өөрсдөө юуг ч зэрэг болгодоггүй. await fetch(...) бол "энэ coroutine-ийг ажиллуулж, дуустал хүлээ" гэсэн үг. main эхний fetch-ийг хүлээж, дараа нь хоёр дахийг, дараа нь гурав дахийг. await бүрт event loop ээлж авсан, гэвч ажиллуулах өөр ажил байгаагүй: мастер ганцхан ширээтэй, өрсөлдөгч бодож байхад хийх зүйлгүй зогссон.
Зэрэгцээ ажиллагаа бий болохын тулд loop-д олон ширээ өгөх хэрэгтэй.
asyncio.gather() олон coroutine-ийг нэг дор loop-д өгч, бүгд дуустал хүлээнэ:
python
async def main():
start = time.perf_counter()
results = await asyncio.gather(
fetch("хуудас-1"),
fetch("хуудас-2"),
fetch("хуудас-3"),
)
print(*results, sep="\n")
print(f"{time.perf_counter() - start:.2f} с")
asyncio.run(main())хуудас-1: эхэллээ
хуудас-2: эхэллээ
хуудас-3: эхэллээ
хуудас-1: дууслаа
хуудас-2: дууслаа
хуудас-3: дууслаа
хуудас-1: татагдлаа
хуудас-2: татагдлаа
хуудас-3: татагдлаа
1.00 сНэг секунд. Хэвлэлтийн дарааллыг Хичээл 14.1-ийн generator loop-тэй харьцуул. Хуудас-1 эхэлж, asyncio.sleep-ийн await-д хүрээд ээлжээ өгнө. Loop хуудас-2-ийг эхлүүлнэ, тэр ч await-д ээлжээ өгнө. Хуудас-3 мөн. Одоо гурвуулаа хүлээж байна, loop-д ажиллуулах юм алга, тэр нэг секунд "унтана". Унтах хугацаа дуусахад гурвууланг ээлжлэн сэрээж, тус бүр "дууслаа" хэвлээд return хийнэ. gather үр дүнг оролтын дарааллаар буцаана, ThreadPoolExecutor.map-ийн адил.
gather-ийг дараагийн хичээлд бүрэн судална, түүний ах дүүс Task ба TaskGroup-ийн хамт. Одоохондоо гол санаа: asyncio-д зэрэгцээ ажиллагааг async def өгдөггүй, олон coroutine-ийг нэг дор loop-д өгөх нь өгдөг.
Thread-ээс asyncio-гийн гол давуу тал нь хэмжээ. Coroutine бол Python объект, үйлдлийн системийн нөөц биш: хэдэн килобайт санах ой. Хэвлэлтгүй хувилбараар хэмжье:
python
async def quiet_fetch(i):
await asyncio.sleep(1.0)
return i
async def main(count):
start = time.perf_counter()
results = await asyncio.gather(*(quiet_fetch(i) for i in range(count)))
print(f"{count:>7,} coroutine: {time.perf_counter() - start:.2f} с, {len(results):,} үр дүн")
for count in (10, 1_000, 10_000, 100_000):
asyncio.run(main(count))*(...) нь Хичээл 5.1-ийн од: generator-ийн элемент бүрийг gather-ийн тусдаа аргумент болгоно. Нэг компьютер дээрх нэг ажиллуулалт:
10 coroutine: 1.00 с, 10 үр дүн
1,000 coroutine: 1.01 с, 1,000 үр дүн
10,000 coroutine: 1.07 с, 10,000 үр дүн
100,000 coroutine: 1.71 с, 100,000 үр дүнЗуун мянган зэрэг хүлээлт хоёр секунд хүрэхгүй хугацаанд. Нэмэлт 0.7 секунд нь зуун мянган coroutine объект үүсгэж, loop тэднийг ээлжлэх ажил. Зуун мянган thread үүсгэх оролдлого ихэнх компьютерийг гацаах эсвэл үйлдлийн системийн хязгаарт хүрч алдаа өгнө. Олон мянган зэрэг сүлжээний холболттой сервер, мянга мянган хүсэлт илгээх клиент бол asyncio-гийн үндсэн талбар.
Симултаны хоёр дахь дүрмийг санаарай: мастер нэг ширээнд удаан бодвол бусад бүгд хүлээнэ. asyncio-д энэ нь: coroutine await-гүйгээр удаан ажил хийвэл event loop бүхэлдээ зогсоно. Хэмжье: fetch-ийн asyncio.sleep-ийг time.sleep-ээр соль:
python
async def blocking_fetch(name, seconds=1.0):
print(f"{name}: эхэллээ")
time.sleep(seconds)
print(f"{name}: дууслаа")
return f"{name}: татагдлаа"
async def main():
start = time.perf_counter()
await asyncio.gather(
blocking_fetch("хуудас-1"),
blocking_fetch("хуудас-2"),
blocking_fetch("хуудас-3"),
)
print(f"{time.perf_counter() - start:.2f} с")
asyncio.run(main())хуудас-1: эхэллээ
хуудас-1: дууслаа
хуудас-2: эхэллээ
хуудас-2: дууслаа
хуудас-3: эхэллээ
хуудас-3: дууслаа
3.00 сgather хэрэглэсэн хэвээр, гэвч гурван секунд, хэвлэлт дараалсан. time.sleep бол await биш: тэр loop-д "би хүлээж байна" гэж хэлэхгүй, thread-ийг бүхэлд нь зогсооно. asyncio ганц thread дээр ажилладаг тул тэр thread зогсвол loop ч зогсоно. Хуудас-1 бүтэн секунд мастерыг ширээнээсээ салгаагүй. Ямар ч алдааны мессеж гараагүйг анзаараарай: програм зөв үр дүн өгсөн, зүгээр л удаан. Энэ бол asyncio-гийн хамгийн түгээмэл бөгөөд хамгийн чимээгүй алдаа.
Ижил дүрэм CPU-bound ажилд ч хамаарна: Хичээл 13.1-ийн crunch-ийг coroutine дотор дуудвал тэр 0.3 секундэд бусад бүх coroutine хүлээнэ. asyncio бол I/O-bound ажлын хэрэгсэл. Coroutine дотор удаан, блоклох ажил хийх зайлшгүй үед, жишээлбэл async хувилбаргүй хуучин сан дуудах, тэр ажлыг Бүлэг 13-ын thread эсвэл process руу шилжүүлэх аргыг дараагийн хичээлүүдэд харна.
Нэгтгэе. Thread-д солилтыг үйлдлийн систем, аль ч мөчид хийдэг. asyncio-д солилт зөвхөн await-д. Тэгэхээр хоёр await-ийн хоорондох код хэзээ ч тасалдахгүй: мастер нүүдлийн дунд ширээнээс холдохгүй. Хичээл 13.2-ын deposit-д уншиж бичих хос await-гүй байсан бол asyncio-д race condition үүсэхгүй. Гэхдээ болгоомжтой: хэрэв read ба write хооронд await байвал, жишээлбэл мэдээллийн сангаас шалгах, тэр await солилтын цэг бөгөөд race condition буцаж ирнэ. Ялгаа нь солилтын цэгүүд кодонд харагддаг: await гэдэг үгийг хайхад л хангалттай.
Үнэ нь: бүх код хамтран ажиллах ёстой. Ганц блоклох дуудлага, time.sleep, requests.get шиг синхрон сүлжээний сан, урт тооцоолол, бүх програмыг зогсооно. Thread-д ийм дуудлага зөвхөн өөрийн thread-ийг зогсоодог байсан.
Хүсвэл Хичээл 14.1-ийн generator loop-д ажил бүрийн алхамд өөр өөр "хүлээх" тоо нэмж, loop тухайн ажлыг тэр тооны эргэлтэд алгасдаг болгож үзээрэй: энэ бол жинхэнэ event loop-ийн "хэзээ сэрээх" мэдлэгийн загвар. Сонирхвол Python-ий тусгай REPL-ийг туршиж болно: терминалд python -m asyncio гэж ажиллуулбал await-ийг шууд, async функцгүйгээр бичиж болох интерактив орчин нээгдэнэ, туршилтад маш тохиромжтой. Мөн хэмжилтийн хэсгийн count-ыг өсгөж, өөрийн компьютер хэдэн coroutine-ийг нэг секундийн ойролцоо барьж чадахыг олох нь сонирхолтой. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
RuntimeWarning: coroutine ‘fetch’ was never awaited. “fetch coroutine хэзээ ч await хийгдсэнгүй” гэсэн утгатай, ихэвчлэн “Enable tracemalloc to get the object allocation traceback” гэсэн нэмэлт мөртэй. Coroutine function-ийг await-гүйгээр дуудахад гарна: fetch("хуудас-1") coroutine объект үүсгэсэн, гэвч хэн ч ажиллуулаагүй. Энэ бол анхааруулга, алдаа биш: програм үргэлжилнэ, гэвч тэр ажил хэзээ ч хийгдээгүй. Coroutine дуудлага бүрийн өмнө await байгаа, эсвэл gather шиг хэрэгсэлд өгсөн эсэхийг шалга.
SyntaxError: ‘await’ outside async function. “await-ийг async функцээс гадна хэрэглэлээ” гэсэн утгатай. await-ийг энгийн def функц дотор бичихэд гарна. Модулийн үндсэн түвшинд бичвэл мессеж нь “‘await’ outside function”. await зөвхөн async def дотор ажиллана. Энэ шаардлага асинхрон байдлыг "халдварт" болгодог: await хэрэглэдэг функц async байх ёстой, түүнийг дууддаг функц ч await хэрэглэх тул async байх ёстой, гэх мэтээр main хүртэл. Тиймээс asyncio програмын бүтэц ихэвчлэн main coroutine ба нэг asyncio.run.
RuntimeError: asyncio.run() cannot be called from a running event loop. "Ажиллаж буй event loop-оос asyncio.run() дуудах боломжгүй" гэсэн утгатай. Jupyter notebook-д ихэвчлэн гарна: notebook өөрөө event loop ажиллуулж байдаг. Тэнд asyncio.run-ийн оронд шууд await main() бичнэ: notebook үндсэн түвшний await-ийг зөвшөөрдөг.
<_GatheringFuture pending> хэвлэгдэх. Мессежгүй, буруу үр дүн. gather-ийн өмнө await-ийг мартвал results хувьсагч үр дүнгийн жагсаалтын оронд "хүлээгдэж буй" объект авна. Хамгийн муу нь, main түүнийг хүлээлгүй дуусах тул coroutine-ууд ч дуусахгүй байж болно. gather-ийн өмнө үргэлж await.
Loop-ийг блоклох. Мессежгүй, хэмжилтээр харсан. time.sleep, синхрон сүлжээний сан, урт тооцоолол coroutine дотор бүх програмыг зогсооно. asyncio програм хүлээснээс удаан бол эхлээд await-гүй удаан дуудлагыг хай. Хүлээх бол asyncio.sleep, сүлжээ бол async сан.
asyncio бол ганц thread дээрх cooperative multitasking: ажлууд ээлжээ зөвхөн await-д, сайн дураараа өгдөг. Event loop ажлуудыг ээлжлэн ажиллуулдаг давталт бөгөөд үндсэн санаа нь Бүлэг 2-ын generator-ийн зогсоох товч дээр тогтдог: бид deque ба next()-ээр хорин мөрт event loop бичсэн. async def coroutine function тодорхойлно. Түүнийг дуудахад функц ажиллахгүй, coroutine объект буцна, яг generator шиг. await нь coroutine-ийг ажиллуулж, дуустал хүлээж, үр дүнг буцаана, хүлээх хооронд loop өөр ажил хийж болно. asyncio.run() loop үүсгэж, main coroutine-ийг ажиллуулна. Дараалсан await-ууд дараалсан хэвээр: async код автоматаар зэрэг ажилладаггүй. Олон coroutine-ийг loop-д нэг дор өгөхөд, жишээлбэл gather-ээр, хүлээлтүүд давхцана. Coroutine хөнгөн: зуун мянган зэрэг хүлээлт хоёр секунд хүрэхгүй. Хамгийн чухал дүрэм: loop-ийг бүү блокло. await-гүй хүлээлт эсвэл тооцоолол бүх coroutine-ийг зогсооно, ямар ч мессежгүйгээр. Солилт зөвхөн await-д явагддаг тул race condition-ий эрсдэл бага, солилтын цэгүүд кодонд харагдана.
Энэ хичээлийн gather зөвхөн урьдчилсан харц байсан. Дараагийн хичээлд asyncio-гийн ажил удирдах хэрэгслүүдийг бүрэн эзэмшинэ: Task нь coroutine-ийг "одоо хүлээхгүй, цаана нь эхлүүл" гэж loop-д өгдөг, ThreadPoolExecutor-ийн submit ба Future-тэй маш төстэй. TaskGroup нь Python 3.11-ээс хойшхи хамгийн аюулгүй арга: олон task-ийг бүлэглэж, нэг нь алдаа гаргавал бусдыг цэвэрхэн цуцалж, алдааг зөв дамжуулна. Мөн нэг ажил хэт удаан хүлээлгэх үед түүнийг хугацаагаар хязгаарлах timeout.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.