aidoc.uz

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.

▶ Video dars· Corey Schafer· 1:42:41· ingliz tilida
Python Tutorial: AsyncIO - Complete Guide to Asynchronous Programming with Animations

asyncio'ning to'liq qo'llanmasi: event loop, korutinalar, vazifalar, gather — animatsiyalar bilan. Uzun, lekin shu mavzudagi eng yaxshi tushuntirishlardan biri.

YouTube'da ochish ↗

1. Ikki xil "sekinlik"#

Optimallashtirishdan oldin dastur nimani kutayotganini aniqlang:

I/O-boundCPU-bound
Vaqt qayerga ketadiTashqi narsani kutishga: tarmoq, disk, ma'lumotlar bazasiHisoblashga: protsessor band
MisollarAPI chaqiruvlari (LLM!), fayl yuklash, veb-skrepingMatn tokenizatsiyasi (sof Python), rasm qayta ishlash, sof Python matematikasi
Protsessor holatiBo'sh turadi100% band
Yechimthreading yoki asynciomultiprocessing 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):

UsulVaqt
Ketma-ket1.31 s
4 ta thread1.31 s — tezlashuv yo'q (GIL)
4 ta process0.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.5s

with 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 (chunksize vazifalarni 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 mumkin

Yechim — 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 += 1

Yaxshiroq 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.

Vazifa A: so'rov yubordi
await — javobni kutadi, boshqaruvni beradi
Event loop
qaysi vazifa davom eta oladi?
Vazifa B: so'rov yubordi
await — u ham kutadi
A'ning javobi keldi
loop A'ni davom ettiradi
Threadsasyncio
Kim almashtiradiOperatsion tizim — istalgan paytdaKodning o'zi — faqat await nuqtalarida
Bir vaqtdagi vazifalarO'nlab–yuzlab (har biri xotira oladi)Minglab–o'n minglab (juda yengil)
Poyga holatiHar qanday qatorda mumkinFaqat await nuqtalarida — ancha kam
KutubxonalarHar qanday (oddiy, sinxron)Async-mos kutubxona kerak (httpx, AsyncAnthropic)
QachonMavjud sinxron kod, kam vazifaKo'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 nuqtasi

Muhim tushunchalar:

  • async def funksiyani chaqirish uni bajarmaydi — korutina obyekti qaytaradi (generatorlarga o'xshash, 7-bob). U faqat await qilinganda yoki vazifa sifatida rejalashtirilganda ishlaydi.
  • await faqat async def ichida 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 emas

Xatolar 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 = None

Tarmoq 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 AsyncAnthropic klienti 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'z idsini 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#

Vazifa sekinmi?
Avval o'lchang (25-bob: profiling). Taxmin qilmang.
Kutish (tarmoq, disk)? → I/O-bound
Async kutubxona bormi va kod yangi → asyncio + Semaphore. Mavjud sinxron kod → ThreadPoolExecutor.
Hisoblash? → CPU-bound
Avval vektorlashtirish (NumPy/Pandas, 19–20-boblar) — ko'pincha 10–100×. Sof Python qolsa → ProcessPoolExecutor.
Ko'p mashina kerakmi?
Ray, Dask, Spark yoki navbat tizimlari (Celery) — bu kursdan tashqarida

Nima noto'g'ri ketishi mumkin#

XatoBelgisiTuzatish
CPU ishi uchun threadlarTezlashuv yo'qProcess'lar yoki NumPy
if __name__ == "__main__" yo'qmacOS/Windows'da process'lar portlashi, RuntimeErrorKirish nuqtasini himoyalang
Process'ga lambda/ochiq fayl uzatishPicklingErrorModul darajasidagi funksiya, oddiy ma'lumot
Umumiy o'zgaruvchiga lock'siz yozishBa'zan noto'g'ri natijaNatijalarni qaytaring yoki Lock/Queue
await unutilgancoroutine was never awaitedKorutinani await qiling
Async ichida time.sleep/requestsHamma vazifalar to'xtaydiasyncio.sleep, httpx.AsyncClient, to_thread
Limitsiz gather 10 000 vazifa429, xotira, ulanishlar tugashiSemaphore
Timeout yo'qPipeline osilib qoladiasyncio.timeout, klient timeout'i
Har so'rovga yangi klientSekin, ulanishlar tugaydiBitta umumiy klient
Jupyter'da asyncio.runcannot be called from a running event loopawait main()
Amaliy topshiriq

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)).

Amaliy topshiriq

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).

Amaliy topshiriq

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.

Amaliy topshiriq

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.

Yodda saqlang
  • 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.

Test: topshirilmaganMasalalar: 0/3

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.