3-qism · Professional Python·17-bob
Parallellik va asyncio: bir vaqtda ko'p ish
I/O-bound va CPU-bound farqi, GIL va free-threaded Python, threading va ThreadPoolExecutor, multiprocessing, asyncio: event loop, async/await, gather, Semaphore bilan limit, TaskGroup, timeout — va yuzlab LLM so'rovlarini parallel yuborish.
Tasavvur qiling: 10 000 ta mijoz sharhini LLM yordamida tasniflashingiz kerak. Har bir so'rov ~1 soniya. Ketma-ket — 2 soat 47 daqiqa. 20 ta parallel so'rov bilan — 8 daqiqa. Bu bob aynan shu farq haqida: qachon va qanday qilib bir vaqtda ko'p ish bajarish. Bu AI muhandisining eng amaliy ko'nikmalaridan biri — batch tasniflash, embedding hisoblash, ko'p hujjatni qayta ishlash, agentlarning parallel asbob chaqiruvlari.
asyncio'ning to'liq qo'llanmasi: event loop, korutinalar, vazifalar, gather — animatsiyalar bilan. Uzun, lekin shu mavzudagi eng yaxshi tushuntirishlardan biri.
1. Ikki xil "sekinlik"#
Optimallashtirishdan oldin dastur nimani kutayotganini aniqlang:
| I/O-bound | CPU-bound | |
|---|---|---|
| Vaqt qayerga ketadi | Tashqi narsani kutishga: tarmoq, disk, ma'lumotlar bazasi | Hisoblashga: protsessor band |
| Misollar | API chaqiruvlari (LLM!), fayl yuklash, veb-skreping | Matn tokenizatsiyasi (sof Python), rasm qayta ishlash, sof Python matematikasi |
| Protsessor holati | Bo'sh turadi | 100% band |
| Yechim | threading yoki asyncio | multiprocessing yoki vektorlashtirish (NumPy) / C kutubxonalar |
LLM bilan ishlash deyarli har doim I/O-bound: so'rovni yuborasiz va model javob berguncha (soniyalar) dasturingiz shunchaki kutadi. Bu kutish vaqtida boshqa so'rovlarni yuborish mumkin.
2. GIL — Global Interpreter Lock#
CPython'da GIL bor: bir vaqtning o'zida faqat bitta thread Python bytecode'ini bajara oladi. Bu xotira boshqaruvini (reference counting, 2-bob) sodda va xavfsiz qiladi, lekin oqibati muhim:
- I/O-bound ishlarda threadlar yaxshi ishlaydi: thread tarmoqni kutayotganda GIL'ni bo'shatadi, boshqa thread ishlaydi.
- CPU-bound sof Python kodida threadlar tezlashtirmaydi: ular navbat bilan bitta GIL uchun kurashadi.
Real o'lchov (4 ta bir xil CPU vazifasi, 11 yadroli kompyuter):
| Usul | Vaqt |
|---|---|
| Ketma-ket | 1.31 s |
| 4 ta thread | 1.31 s — tezlashuv yo'q (GIL) |
| 4 ta process | 0.39 s — ~3.4× tezroq |
3. concurrent.futures — eng oson boshlanish#
Standart kutubxonadagi bu modul thread va process'lar uchun bir xil, sodda interfeys beradi.
ThreadPoolExecutor — I/O-bound uchun#
import time
from concurrent.futures import ThreadPoolExecutor
def fetch(i: int) -> int:
time.sleep(0.5) # tarmoq so'rovi o'rnida
return i * i
start = time.perf_counter()
results = [fetch(i) for i in range(10)]
print(f"ketma-ket: {time.perf_counter() - start:.2f}s") # ~5.0s
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=10) as ex:
results = list(ex.map(fetch, range(10))) # tartib saqlanadi
print(f"threadlar: {time.perf_counter() - start:.2f}s") # ~0.5swith bloki tugaganda executor barcha vazifalar tugashini kutadi va
threadlarni yopadi.
Natijalarni tayyor bo'lishi bilan olish#
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=8) as ex:
futures = {ex.submit(process_document, doc): doc.id for doc in docs}
for fut in as_completed(futures): # qaysi biri birinchi tugasa
doc_id = futures[fut]
try:
result = fut.result() # vazifadagi xato shu yerda ko'tariladi
except Exception as e:
log.warning("%s: %r", doc_id, e)
continue
save(doc_id, result)submit — Future obyekti qaytaradi: "kelajakda tayyor bo'ladigan
natija" va'dasi. fut.result() — natijani kutib oladi; vazifada xato
bo'lgan bo'lsa — o'sha xatoni ko'taradi.
ProcessPoolExecutor — CPU-bound uchun#
from concurrent.futures import ProcessPoolExecutor
def tokenize_file(path: str) -> int:
with open(path, encoding="utf-8") as f:
return sum(len(line.split()) for line in f) # sof Python — CPU ishi
if __name__ == "__main__": # ← process'lar uchun SHART
with ProcessPoolExecutor() as ex: # standart: yadrolar soni
counts = list(ex.map(tokenize_file, paths, chunksize=16))Process'lar haqida bilish kerak bo'lgan narsalar:
- har bir process — alohida Python interpretatori, o'z xotirasi va o'z GIL'i;
- argumentlar va natijalar pickle (11-bob) orqali process'lar orasida ko'chiriladi — lambda, ochiq fayl, ulanishlarni uzatib bo'lmaydi; katta ma'lumotni uzatish qimmat;
- macOS va Windows'da yangi process modulni qayta import qiladi —
if __name__ == "__main__":himoyasisiz cheksiz process yaratilishi mumkin; - ishga tushirish xarajati bor — juda kichik vazifalar uchun foydasiz
(
chunksizevazifalarni guruhlaydi).
4. threading — past daraja va umumiy holat#
Executor'lar ko'p hollarda yetarli. Lekin threadlar umumiy xotirani baham ko'rgani uchun bitta xavf bor — poyga holati (race condition):
import threading
counter = 0
def increment(n: int) -> None:
global counter
for _ in range(n):
counter += 1 # o'qish → qo'shish → yozish: atomik EMAS
threads = [threading.Thread(target=increment, args=(100_000,)) for _ in range(8)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 800000 bo'lishi kerak — ba'zan kamroq chiqishi mumkinYechim — qulf (Lock):
lock = threading.Lock()
def increment(n: int) -> None:
global counter
for _ in range(n):
with lock: # bir vaqtda faqat bitta thread kiradi
counter += 1Yaxshiroq yechim — umumiy holatdan umuman qochish: har bir vazifa
natijasini qaytarsin, asosiy thread ularni yig'sin (executor.map
aynan shunday). Threadlar orasida ma'lumot uzatish kerak bo'lsa —
thread-xavfsiz queue.Queue.
5. asyncio — bitta thread, minglab vazifa#
asyncio — I/O-bound ish uchun boshqa yondashuv: bitta threadda ko'p
vazifani almashlab bajarish. Har bir vazifa kutish nuqtasiga (await)
yetganda boshqaruvni ixtiyoriy ravishda qaytaradi va event loop
boshqa tayyor vazifani davom ettiradi.
| Threads | asyncio | |
|---|---|---|
| Kim almashtiradi | Operatsion tizim — istalgan paytda | Kodning o'zi — faqat await nuqtalarida |
| Bir vaqtdagi vazifalar | O'nlab–yuzlab (har biri xotira oladi) | Minglab–o'n minglab (juda yengil) |
| Poyga holati | Har qanday qatorda mumkin | Faqat await nuqtalarida — ancha kam |
| Kutubxonalar | Har qanday (oddiy, sinxron) | Async-mos kutubxona kerak (httpx, AsyncAnthropic) |
| Qachon | Mavjud sinxron kod, kam vazifa | Ko'p tarmoq so'rovi, servislar (FastAPI) |
async def va await#
import asyncio
async def fetch(i: int) -> int: # korutina funksiyasi
await asyncio.sleep(0.5) # kutish — boshqaruv loop'ga qaytadi
return i * i
async def main() -> None:
result = await fetch(3) # bitta korutinani kutish
print(result)
asyncio.run(main()) # event loop'ni ishga tushiradi — dastur kirish nuqtasiMuhim tushunchalar:
async deffunksiyani chaqirish uni bajarmaydi — korutina obyekti qaytaradi (generatorlarga o'xshash, 7-bob). U faqatawaitqilinganda yoki vazifa sifatida rejalashtirilganda ishlaydi.awaitfaqatasync defichida yoziladi.asyncio.run()— sinxron dunyodan async dunyoga "eshik"; odatda dasturda bir marta.
gather — ko'p korutinani parallel kutish#
async def main() -> None:
results = await asyncio.gather(*(fetch(i) for i in range(10)))
print(results) # [0, 1, 4, ...] — tartib saqlanadi; jami ~0.5s, 5s emasXatolar bilan#
results = await asyncio.gather(*tasks, return_exceptions=True)
for r in results:
if isinstance(r, Exception):
log.warning("vazifa yiqildi: %r", r)return_exceptions=Truesiz birinchi xato gatherdan ko'tariladi, lekin
qolgan vazifalar bekor qilinmaydi — fonda ishlashda davom etadi.
TaskGroup — tuzilmali parallellik (3.11+)#
async def main() -> None:
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fetch(1))
t2 = tg.create_task(fetch(2))
# blokdan chiqqanda hammasi tugagan; biri yiqilsa — qolganlari bekor qilinadi
print(t1.result(), t2.result())Xatolar ExceptionGroup (10-bob) sifatida ko'tariladi va except* bilan
ushlanadi. Yangi kodda TaskGroup — "hammasi yoki hech biri" semantikasi
kerak bo'lganda afzal.
Timeout#
async def main() -> None:
try:
async with asyncio.timeout(10): # 3.11+
answer = await call_llm(prompt)
except TimeoutError:
answer = NoneTarmoq so'rovlarida har doim timeout bo'lsin — aks holda bitta osilib qolgan ulanish butun pipeline'ni cheksiz to'xtatishi mumkin.
6. Bir vaqtdagi so'rovlarni cheklash: Semaphore#
10 000 ta so'rovni bir vaqtda yuborsangiz: API rate limit (429 xatolar), xotira to'lishi, ochiq ulanishlar limiti. Bir vaqtda ko'pi bilan N ta bo'lishi kerak:
import asyncio
async def limited_fetch(sem: asyncio.Semaphore, i: int) -> int:
async with sem: # bo'sh "o'rin" bo'lguncha kutadi
await asyncio.sleep(0.5)
return i
async def main() -> None:
sem = asyncio.Semaphore(3) # bir vaqtda 3 ta
await asyncio.gather(*(limited_fetch(sem, i) for i in range(9))) # ~1.5s: 3 to'lqin
asyncio.run(main())Bu — AI ilovalarida eng ko'p ishlatiladigan naqsh. Provayderingizning
limitlariga (minutiga so'rovlar va tokenlar) qarab Semaphore
qiymatini tanlang.
7. Amaliyot: ko'p sharhni LLM bilan parallel tasniflash#
To'liq, production uslubidagi misol — 1-bobda va'da qilingan kodning mukammal versiyasi:
import asyncio
import json
import logging
from pathlib import Path
import anthropic
from anthropic import AsyncAnthropic
log = logging.getLogger(__name__)
LABELS = {"positive", "negative", "neutral"}
PROMPT = ("Quyidagi sharhning kayfiyatini aniqla. Faqat bitta so'z bilan javob ber: "
"positive, negative yoki neutral.\n\nSharh: {text}")
async def classify(client: AsyncAnthropic, sem: asyncio.Semaphore, item: dict) -> dict:
async with sem:
try:
msg = await client.messages.create(
model="claude-opus-5-5",
max_tokens=16,
messages=[{"role": "user", "content": PROMPT.format(text=item["text"])}],
)
except anthropic.APIError as e:
log.warning("id=%s: %r", item["id"], e)
return {**item, "label": None, "error": type(e).__name__}
text = "".join(b.text for b in msg.content if b.type == "text").strip().lower()
return {**item, "label": text if text in LABELS else "neutral"}
async def classify_all(items: list[dict], concurrency: int = 10) -> list[dict]:
sem = asyncio.Semaphore(concurrency)
# SDK o'zi 429/5xx xatolarda backoff bilan qayta urinadi (max_retries)
async with AsyncAnthropic(max_retries=4, timeout=60) as client:
tasks = [classify(client, sem, it) for it in items]
results = []
for i, coro in enumerate(asyncio.as_completed(tasks), 1): # tayyor bo'lishi bilan
results.append(await coro)
if i % 100 == 0:
log.info("%d/%d tayyor", i, len(items))
return results
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
items = [json.loads(line) for line in Path("reviews.jsonl").read_text(encoding="utf-8").splitlines()]
results = asyncio.run(classify_all(items, concurrency=10))
failed = [r for r in results if r.get("error")]
Path("labeled.jsonl").write_text(
"".join(json.dumps(r, ensure_ascii=False) + "\n" for r in results), encoding="utf-8"
)
log.info("Tayyor: %d ta, %d ta xato", len(results), len(failed))
if __name__ == "__main__":
main()Bu kodda nimalar muhim:
- bitta
AsyncAnthropicklienti barcha so'rovlar uchun — ulanishlar qayta ishlatiladi (har so'rovga yangi klient yaratmang); Semaphore— bir vaqtdagi so'rovlar limiti;- SDK'ning o'rnatilgan retry'i (
max_retries) vaqtinchalik xatolarni hal qiladi; qolgan xatolar har bir yozuvda ushlanib, natijada qayd etiladi — bitta xato butun ishni to'xtatmaydi; as_completed— progress'ni ko'rsatish uchun; natijalar tartibi aralashadi, shuning uchun har bir natija o'zidsini olib yuradi;- model javobi tekshiriladi va kutilmagan javob xavfsiz qiymatga aylanadi.
8. Async'ni boshqa dunyolar bilan bog'lash#
# Async ichidan sinxron (bloklovchi) funksiya — threadda
result = await asyncio.to_thread(heavy_sync_function, arg)
# Async ichidan CPU-og'ir ish — process'da
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
result = await loop.run_in_executor(pool, cpu_heavy, data)
# Ishlab chiqaruvchi–iste'molchi: asyncio.Queue
queue: asyncio.Queue[str] = asyncio.Queue(maxsize=100)Jupyter'da: notebook'da event loop allaqachon ishlab turadi — shuning
uchun asyncio.run() xato beradi (asyncio.run() cannot be called from a running event loop). Jupyter'da to'g'ridan-to'g'ri await main() deb
yozing.
9. Qaysi birini tanlash kerak#
Nima noto'g'ri ketishi mumkin#
| Xato | Belgisi | Tuzatish |
|---|---|---|
| CPU ishi uchun threadlar | Tezlashuv yo'q | Process'lar yoki NumPy |
if __name__ == "__main__" yo'q | macOS/Windows'da process'lar portlashi, RuntimeError | Kirish nuqtasini himoyalang |
| Process'ga lambda/ochiq fayl uzatish | PicklingError | Modul darajasidagi funksiya, oddiy ma'lumot |
| Umumiy o'zgaruvchiga lock'siz yozish | Ba'zan noto'g'ri natija | Natijalarni qaytaring yoki Lock/Queue |
await unutilgan | coroutine was never awaited | Korutinani await qiling |
Async ichida time.sleep/requests | Hamma vazifalar to'xtaydi | asyncio.sleep, httpx.AsyncClient, to_thread |
Limitsiz gather 10 000 vazifa | 429, xotira, ulanishlar tugashi | Semaphore |
| Timeout yo'q | Pipeline osilib qoladi | asyncio.timeout, klient timeout'i |
| Har so'rovga yangi klient | Sekin, ulanishlar tugaydi | Bitta umumiy klient |
Jupyter'da asyncio.run | cannot be called from a running event loop | await main() |
1. O'lchab ko'ring
Bir xil 8 ta vazifani (a) time.sleep(1) va (b) og'ir sof Python hisob
uchun ketma-ket, 8 thread va 8 process bilan bajaring. Natijalarni
jadvalga yozing va GIL nuqtai nazaridan tushuntiring.
Maslahatni ko'rsatish
time.perf_counter() bilan har variantni o'lchang; sof Python CPU funksiyasi: sum(i*i for i in range(n)).
2. Async veb yuklovchi
httpx.AsyncClient bilan 20 ta URL'ni bir vaqtda ko'pi bilan 5 tadan
yuklang; har biri uchun status kodi, hajmi va vaqtini chiqaring; xato
bergan URL'lar dasturni to'xtatmasin. Ketma-ket versiya bilan vaqtni
solishtiring.
Maslahatni ko'rsatish
async with httpx.AsyncClient(timeout=10) as client; Semaphore(5); gather(..., return_exceptions=True); hajm — len(resp.content).
3. Soxta LLM bilan rate limit
Soxta async LLM klassini yozing: complete 0.2–0.8 soniya "o'ylasin"
va bir vaqtdagi faol chaqiruvlar sonining maksimumini qayd etsin.
classify_allni unga moslab, concurrency=5 berilganda maksimum hech
qachon 5 dan oshmasligini test bilan tekshiring (pytest + asyncio.run).
Maslahatni ko'rsatish
FakeAsyncLLM.complete ichida: self.active += 1; max_active = max(...); await asyncio.sleep(random...); self.active -= 1.
4. Producer–consumer pipeline
asyncio.Queue bilan uch bosqichli pipeline yozing: (1) fayldan qatorlarni
o'qib navbatga qo'yuvchi, (2) 4 ta "worker" — har biri navbatdan olib,
soxta LLM'ga yuborib, natijani ikkinchi navbatga qo'yadi, (3) natijalarni
faylga yozuvchi. Navbatlarga maxsize bering va nega bu muhimligini
(backpressure) tushuntiring.
- 01Avval aniqlang: I/O-bound (kutish) yoki CPU-bound (hisoblash).
- 02GIL: sof Python CPU ishida threadlar tezlashtirmaydi; I/O'da — tezlashtiradi.
- 03ThreadPoolExecutor — I/O; ProcessPoolExecutor — CPU (pickle, __main__ himoyasi).
- 04asyncio: bitta thread, await nuqtalarida almashinadi; async def chaqirilsa — korutina, natija emas.
- 05gather / TaskGroup — parallel kutish; asyncio.timeout — har doim timeout.
- 06Semaphore — bir vaqtdagi so'rovlar limiti: LLM bilan ishlashning asosiy naqshi.
- 07Async ichida bloklovchi chaqiruv qilmang: asyncio.sleep, async klientlar, to_thread.
- 08Bitta umumiy klient, SDK retry'i, har yozuv uchun xatoni ushlash; tezlik kerak bo'lmasa — Batch API.
Mustahkamlash
Test va amaliy masalalar
Avval testni ishlang — har javobdan keyin izoh chiqadi. Keyin masalalarni shu yerning o'zida yechib, brauzerdagi haqiqiy Python bilan avtomatik tekshiring. Natijalaringiz kurs progressi sahifasida jamlanadi.
Bilimni tekshirish
Bob bo'yicha test
12 ta savol · har javobdan keyin to'g'ri yoki xatoligi va izoh ko'rsatiladi · oxirida natija foizda.
Amaliy masalalar
Kodni yozing va «Tekshirish»ni bosing (yoki Ctrl/⌘ + Enter). Kodingiz shu brauzerda saqlanib turadi.
01Parallel 'yuklab olish'
fetch_all(urls, fetch, limit) — async funksiya: har URL uchun await fetch(url) ni bir vaqtda ko'pi bilan limit tadan bajarib, natijalarni urls tartibida qaytarsin. fetch xato bersa, o'sha joyda xato obyekti tursin.
02Timeout bilan chaqiruv
with_timeout(coro_fn, seconds, default=None) — async funksiya: await coro_fn() ni seconds ichida bajarsin, ulgurmasa default qaytarsin (asyncio.timeout yoki asyncio.wait_for).
03Producer–consumer
pipeline(items, worker, n_workers=3) — async: asyncio.Queue ga elementlarni qo'yib, n_workers ta iste'molchi await worker(x) natijalarini yig'sin. Natijani saralangan ro'yxat qilib qaytaring.