Parallellik: thread, process va asyncio
Uchta vosita, uchta vazifa
Python da parallel ishlashning uch yo'li bor va ularni tanlash xato qilish oson bo'lgan joy. Qoida bitta savolga bog'liq: dasturingiz nimani kutmoqda?
| Vazifa turi | Nimani kutadi | To'g'ri vosita |
|---|---|---|
| I/O-bog'liq | Tarmoq, disk, ma'lumotlar bazasi | threading yoki asyncio |
| CPU-bog'liq | Hisob-kitob | multiprocessing |
Sabab — GIL (Global Interpreter Lock): bir vaqtda faqat bitta oqim Python bayt-kodini bajaradi. Demak thread lar hisobni tezlashtirmaydi. Lekin kutish paytida GIL bo'shatiladi, shuning uchun I/O da thread lar yordam beradi.
Buni o'lchaymiz.
GIL ni ko'rsatamiz: CPU-bog'liq ish
ProcessPoolExecutor funksiyani boshqa jarayonga pickle qilib yuboradi, shuning uchun u
import qilinadigan modulda bo'lishi shart. Alohida faylga yozamiz:
shu faylni yaratingogir_ish.py
"""CPU-bog'liq ish. ProcessPoolExecutor uchun alohida modulda bo'lishi SHART.""" def ogir_hisob(n: int) -> int: """Sof hisob: GIL ni ushlab turadi.""" s = 0 for i in range(n): s += i * i return s
import sys, time sys.path.insert(0, str(ISH)) from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor from ogir_ish import ogir_hisob N = 3_000_000 ISH_SONI = 4 t0 = time.perf_counter() [ogir_hisob(N) for _ in range(ISH_SONI)] ketma_ket = time.perf_counter() - t0 t0 = time.perf_counter() with ThreadPoolExecutor(max_workers=4) as p: list(p.map(ogir_hisob, [N] * ISH_SONI)) threadlar = time.perf_counter() - t0 t0 = time.perf_counter() with ProcessPoolExecutor(max_workers=4) as p: list(p.map(ogir_hisob, [N] * ISH_SONI)) jarayonlar = time.perf_counter() - t0 import pandas as pd show(pd.DataFrame([ {"usul": "ketma-ket", "vaqt, s": round(ketma_ket, 2), "tezlanish": "1.00x"}, {"usul": "4 thread", "vaqt, s": round(threadlar, 2), "tezlanish": f"{ketma_ket / threadlar:.2f}x"}, {"usul": "4 jarayon", "vaqt, s": round(jarayonlar, 2), "tezlanish": f"{ketma_ket / jarayonlar:.2f}x"}, ])) import os print("mavjud yadrolar:", os.cpu_count())
mavjud yadrolar: 8
| usul | vaqt, s | tezlanish |
|---|---|---|
| ketma-ket | 0.31 | 1.00x |
| 4 thread | 0.3 | 1.02x |
| 4 jarayon | 0.09 | 3.59x |
Thread lar CPU ishida deyarli hech narsa bermadi — GIL sababli. Jarayonlar esa haqiqiy tezlanish berdi, chunki har biri o'z Python interpretatoriga ega.
I/O-bog'liq ish: thread lar yutadi
import sys, time sys.path.insert(0, str(ISH)) from concurrent.futures import ThreadPoolExecutor import urllib.request from sinov_server import ishga_tushir server2, manzil2 = ishga_tushir() def sekin_sorov(_): """Har so'rov taxminan 0.2 s kutadi (server tomonida).""" with urllib.request.urlopen(manzil2 + "/ok", timeout=10) as j: time.sleep(0.2) # tarmoq kechikishini taqlid qilamiz return j.status SORO_SONI = 12 t0 = time.perf_counter() [sekin_sorov(i) for i in range(SORO_SONI)] ketma_ket_io = time.perf_counter() - t0 natijalar = {} for ishchilar in (2, 4, 12): t0 = time.perf_counter() with ThreadPoolExecutor(max_workers=ishchilar) as p: list(p.map(sekin_sorov, range(SORO_SONI))) natijalar[ishchilar] = time.perf_counter() - t0 import pandas as pd qatorlar = [{"usul": "ketma-ket", "vaqt, s": round(ketma_ket_io, 2), "tezlanish": "1.00x"}] for n, v in natijalar.items(): qatorlar.append({"usul": f"{n} thread", "vaqt, s": round(v, 2), "tezlanish": f"{ketma_ket_io / v:.2f}x"}) show(pd.DataFrame(qatorlar))
| usul | vaqt, s | tezlanish |
|---|---|---|
| ketma-ket | 2.41 | 1.00x |
| 2 thread | 1.21 | 2.00x |
| 4 thread | 0.61 | 3.97x |
| 12 thread | 1.26 | 1.91x |
Mana farq: o'sha thread lar I/O da haqiqiy tezlanish berdi. Kutish paytida GIL bo'shaydi va boshqa thread ishlaydi.
Diqqat qiling: 12 thread 4 tadan sekinroq chiqdi (1.94x va 3.98x). Bu kutilmagan, lekin sababi
aniq: bizning sinov serverimiz HTTPServer — u bir vaqtda bitta so'rovni qayta ishlaydi.
4 threaddan keyin bo'g'in mijozda emas, serverda.
Bu amaliy saboq: parallellikni oshirishdan oldin qayerda bo'g'in borligini biling. Ko'pincha u sizning kodingizda emas, siz murojaat qilayotgan tizimda bo'ladi — va u yerga ko'proq bosim berish natijani yomonlashtiradi.
asyncio: bitta thread, minglab kutish
asyncio boshqa yondashuv: bitta thread, lekin kutish paytida boshqa vazifaga o'tadi.
import asyncio, time async def sekin_ish(nom: str, kutish: float) -> str: await asyncio.sleep(kutish) # time.sleep EMAS! return f"{nom} tayyor" async def ketma_ket_async(): t0 = time.perf_counter() for i in range(5): await sekin_ish(f"ish{i}", 0.2) return time.perf_counter() - t0 async def parallel_async(): t0 = time.perf_counter() await asyncio.gather(*(sekin_ish(f"ish{i}", 0.2) for i in range(5))) return time.perf_counter() - t0 k = asyncio.run(ketma_ket_async()) p = asyncio.run(parallel_async()) print(f"await ketma-ket : {k:.2f} s") print(f"asyncio.gather : {p:.2f} s ({k / p:.1f}x tezroq)")
await ketma-ket : 1.00 s asyncio.gather : 0.20 s (5.0x tezroq)
await o'zi parallel qilmaydi — u faqat "bu yerda kutish mumkin" deydi. Parallellik
gather dan keladi. Bu eng ko'p uchraydigan asyncio tushunmovchiligi.
:::gotcha time.sleep async kodni bloklaydi
async def ichida time.sleep(1) yozsangiz, butun tsikl to'xtaydi — boshqa vazifalar ham
kutadi. Har doim await asyncio.sleep(1) ishlating. Xuddi shunday: requests.get bloklaydi,
httpx ning async mijozi esa yo'q.
:::
Buni o'lchaymiz:
import asyncio, time async def togri(_): await asyncio.sleep(0.15) async def notogri(_): time.sleep(0.15) # BLOKLAYDI async def olch(f): t0 = time.perf_counter() await asyncio.gather(*(f(i) for i in range(6))) return time.perf_counter() - t0 print(f"await asyncio.sleep : {asyncio.run(olch(togri)):.2f} s (parallel)") print(f"time.sleep : {asyncio.run(olch(notogri)):.2f} s (ketma-ket, 6 x 0.15)")
await asyncio.sleep : 0.15 s (parallel) time.sleep : 0.90 s (ketma-ket, 6 x 0.15)
Ikkinchisi olti barobar sekin — chunki gather chaqirilgan bo'lsa ham, hech narsa parallel
bo'lmadi.
Umumiy holat va poyga (race condition)
Thread lar bir xotirani bo'lishadi. Bu tez, lekin xavfli:
import threading, time from concurrent.futures import ThreadPoolExecutor class Oddiy: """+= 1 — bir qarashda atomar ko'rinadi.""" def __init__(self): self.qiymat = 0 def oshir(self): self.qiymat += 1 class Ochiq: """O'sha amal, lekin uch qadami ochiq yozilgan.""" def __init__(self): self.qiymat = 0 def oshir(self): tmp = self.qiymat # 1) o'qish time.sleep(0) # 2) shu yerda boshqa thread ishga tushishi mumkin self.qiymat = tmp + 1 # 3) yozish class Qulfli: def __init__(self): self.qiymat = 0 self._qulf = threading.Lock() def oshir(self): with self._qulf: tmp = self.qiymat time.sleep(0) self.qiymat = tmp + 1 def sinov(sanagich, threadlar=8, har_biri=20_000): def ishla(): for _ in range(har_biri): sanagich.oshir() with ThreadPoolExecutor(max_workers=threadlar) as p: list(p.map(lambda _: ishla(), range(threadlar))) return sanagich.qiymat, threadlar * har_biri import pandas as pd qatorlar = [] for nom, sinf in [("`+= 1`", Oddiy), ("o'qish/yozish ochiq", Ochiq), ("qulf bilan", Qulfli)]: olingan, kutilgan = sinov(sinf()) qatorlar.append({"usul": nom, "kutilgan": kutilgan, "olingan": olingan, "yo'qolgan": kutilgan - olingan, "yo'qotish %": round(100 * (kutilgan - olingan) / kutilgan, 1)}) show(pd.DataFrame(qatorlar))
| usul | kutilgan | olingan | yo'qolgan | yo'qotish % |
|---|---|---|---|---|
| `+= 1` | 160 000 | 160 000 | 0 | 0 |
| o'qish/yozish ochiq | 160 000 | 20 059 | 139 941 | 87.5 |
| qulf bilan | 160 000 | 160 000 | 0 | 0 |
Natijaga diqqat bilan qarang, chunki u ikki xil saboq beradi.
+= 1 hech narsa yo'qotmadi. Bu CPython 3.10 da ko'pincha shunday bo'ladi: GIL almashinuvi
odatda shu amalning o'rtasiga tushmaydi. Lekin bu kafolat emas — Python versiyasi, yuklama va
amal turi o'zgarsa, natija ham o'zgaradi.
O'sha amal ochiq yozilganda 87% yo'qoldi. time.sleep(0) — bu kechikish emas, u shunchaki
"boshqa thread ishlasin" deb GIL ni beradi. Ya'ni biz poygani ko'rinadigan qildik, yaratmadik:
u birinchi holatda ham mavjud edi, shunchaki yuzaga chiqmadi.
Mana xavfning mohiyati: poyga holati sinovdan o'tib ketadi va ishlab chiqarishda chiqadi. Testda yo'qotish nolga teng bo'lishi "kod xavfsiz" degani emas.
Qulf esa ikkinchi holatni ham to'liq tuzatdi.
Eng yaxshi yechim — umumiy holatni butunlay yo'q qilish. Har thread o'z natijasini qaytarsin, siz esa oxirida yig'ing:
from concurrent.futures import ThreadPoolExecutor def mustaqil_ish(n): """Umumiy holat yo'q — natijani QAYTARADI.""" return sum(1 for _ in range(n)) with ThreadPoolExecutor(max_workers=8) as p: natijalar = list(p.map(mustaqil_ish, [50_000] * 8)) print("har ishchi qaytardi:", natijalar) print("yig'indi:", sum(natijalar), "| kutilgan:", 8 * 50_000, "| qulf kerak bo'lmadi")
har ishchi qaytardi: [50000, 50000, 50000, 50000, 50000, 50000, 50000, 50000] yig'indi: 400000 | kutilgan: 400000 | qulf kerak bo'lmadi
Loyihaga qo'llaymiz: ko'p faylni qayta ishlash
shu faylni yaratingsrc/hisobot/ko_p.py
"""Bir nechta CSV faylni parallel qayta ishlash.""" import logging from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path from .modellar import Natija from .oqish import oqi, tozala from .xatolar import HisobotXato log = logging.getLogger(__name__) def bitta(yol: Path) -> tuple[Path, Natija | None, str | None]: """Bitta faylni qayta ishlaydi. Xato otmaydi — uni qaytaradi.""" try: return yol, tozala(oqi(yol)), None except HisobotXato as e: return yol, None, str(e) def hammasi(yollar: list[Path], ishchilar: int = 4) -> dict[str, Natija | str]: """Fayllarni parallel qayta ishlaydi. Bitta faylning xatosi boshqalarni to'xtatmaydi.""" natijalar: dict[str, Natija | str] = {} with ThreadPoolExecutor(max_workers=ishchilar) as p: kelajaklar = {p.submit(bitta, y): y for y in yollar} for k in as_completed(kelajaklar): yol, natija, xato = k.result() natijalar[yol.name] = natija if natija is not None else f"XATO: {xato}" log.debug("tugadi: %s", yol.name) return natijalar
mkdir -p ko_p_sinov for i in 1 2 3 4 5 6; do printf 'ism,guruh,baho,sana\nT%s,A,8%s,2026-09-0%s\nX%s,B,9%s,2026-09-0%s\n' $i $i 1 $i $i 2 > ko_p_sinov/f$i.csv done printf 'ism,guruh\nBuzuq,A\n' > ko_p_sinov/buzuq.csv ls ko_p_sinov/
buzuq.csv f1.csv f2.csv f3.csv f4.csv f5.csv f6.csv
import sys, time, importlib from pathlib import Path sys.path.insert(0, str(ISH / "src")) import hisobot.ko_p as ko_p importlib.reload(ko_p) yollar = sorted((ISH / "ko_p_sinov").glob("*.csv")) print("fayllar:", [y.name for y in yollar]) t0 = time.perf_counter() ketma = {y.name: ko_p.bitta(y)[1] for y in yollar} ketma_s = time.perf_counter() - t0 t0 = time.perf_counter() parallel = ko_p.hammasi(yollar, ishchilar=4) parallel_s = time.perf_counter() - t0 print(f"\nketma-ket: {ketma_s * 1000:.0f} ms | parallel: {parallel_s * 1000:.0f} ms") for nom, natija in sorted(parallel.items()): holat = natija if isinstance(natija, str) else f"{len(natija.talabalar)} to'g'ri" print(f" {nom:12} {holat}")
fayllar: ['buzuq.csv', 'f1.csv', 'f2.csv', 'f3.csv', 'f4.csv', 'f5.csv', 'f6.csv'] ketma-ket: 24 ms | parallel: 5 ms buzuq.csv XATO: faylni o'qib bo'lmadi: /mnt/c/Users/Admin/Desktop/Course Material/Python_Muhandisligi/ish/ko_p_sinov/buzuq.csv (ustunlar yetishmaydi: ['baho', 'sana']) f1.csv 2 to'g'ri f2.csv 2 to'g'ri f3.csv 2 to'g'ri f4.csv 2 to'g'ri f5.csv 2 to'g'ri f6.csv 2 to'g'ri
Diqqat: bu yerda fayllar juda kichik, shuning uchun parallellik foyda bermasligi mumkin — thread yaratish xarajati ishdan ko'proq. Bu muhim saboq: parallellik bepul emas.
Buzuq fayl esa boshqalarni to'xtatmadi — uning xatosi natijalar lug'atida qaytdi.
Qachon nimani tanlash
import pandas as pd show(pd.DataFrame([ {"holat": "Bir nechta faylni o'qish", "tanlov": "ThreadPoolExecutor", "sabab": "I/O kutish"}, {"holat": "100 ta HTTP so'rov", "tanlov": "ThreadPoolExecutor yoki asyncio", "sabab": "I/O kutish"}, {"holat": "10 000 ta HTTP so'rov", "tanlov": "asyncio", "sabab": "Thread xotira yeydi"}, {"holat": "Rasmlarni qayta ishlash", "tanlov": "ProcessPoolExecutor", "sabab": "CPU ish, GIL to'siq"}, {"holat": "Katta matritsani hisoblash", "tanlov": "numpy (GIL ni bo'shatadi)", "sabab": "C darajasida parallel"}, {"holat": "Kichik, tez ish", "tanlov": "Oddiy sikl", "sabab": "Parallellik xarajati ishdan ko'p"}, ]))
| holat | tanlov | sabab |
|---|---|---|
| Bir nechta faylni o'qish | ThreadPoolExecutor | I/O kutish |
| 100 ta HTTP so'rov | ThreadPoolExecutor yoki asyncio | I/O kutish |
| 10 000 ta HTTP so'rov | asyncio | Thread xotira yeydi |
| Rasmlarni qayta ishlash | ProcessPoolExecutor | CPU ish, GIL to'siq |
| Katta matritsani hisoblash | numpy (GIL ni bo'shatadi) | C darajasida parallel |
| Kichik, tez ish | Oddiy sikl | Parallellik xarajati ishdan ko'p |
Va eng muhim tavsiya: avval o'lchang. Parallellik kodni murakkablashtiradi; agar u 5% tezlanish bersa, arzimaydi.
Testlar
shu faylni yaratingtests/test_ko_p.py
from hisobot.ko_p import bitta, hammasi from hisobot.modellar import Natija def _fayl(papka, nom, matn): y = papka / nom y.write_text(matn, encoding="utf-8") return y TOGRI = "ism,guruh,baho,sana\nA,X,85,2026-01-01\n" BUZUQ = "ism,guruh\nA,X\n" def test_bitta_togri(tmp_path): _yol, natija, xato = bitta(_fayl(tmp_path, "a.csv", TOGRI)) assert xato is None assert natija is not None assert len(natija.talabalar) == 1 def test_bitta_buzuq(tmp_path): _yol, natija, xato = bitta(_fayl(tmp_path, "b.csv", BUZUQ)) assert natija is None assert "ustunlar yetishmaydi" in xato def test_hammasi_xato_boshqalarni_toxtatmaydi(tmp_path): yollar = [ _fayl(tmp_path, "a.csv", TOGRI), _fayl(tmp_path, "buzuq.csv", BUZUQ), _fayl(tmp_path, "c.csv", TOGRI), ] n = hammasi(yollar, ishchilar=3) assert len(n) == 3 assert isinstance(n["a.csv"], Natija) assert isinstance(n["c.csv"], Natija) assert str(n["buzuq.csv"]).startswith("XATO") def test_hammasi_bosh_royxat(): assert hammasi([]) == {}
.venv/bin/python3 -m pytest tests/test_ko_p.py -q .venv/bin/python3 -m pytest -q python3 -m mypy
.... [100%] ........................................................................ [ 98%] . [100%] Success: no issues found in 12 source files
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| GIL | Bir vaqtda bitta thread bayt-kod bajaradi |
| CPU ish | ProcessPoolExecutor — thread lar yordam bermaydi |
| I/O ish | ThreadPoolExecutor yoki asyncio |
await |
O'zi parallel qilmaydi; parallellik gather dan |
time.sleep async da |
Butun tsiklni bloklaydi — asyncio.sleep ishlating |
| Poyga holati | += 1 atomar emas; qiymat jim yo'qoladi |
| Eng yaxshi yechim | Umumiy holatni yo'q qiling — natijani qaytaring |
| Parallellik narxi | Kichik ishda foyda bermaydi; avval o'lchang |
Talaba ishi
ogir_hisobninumpybilan qayta yozing (np.arange(n) ** 2).sum()) va thread lar bilan sinang. Natija o'zgardimi? Nega?Sanagichsinovini 2 thread bilan takrorlang. Yo'qotish kamaydimi? Bu xatoni nega testda ushlash qiyin?hammasiga 200 ta katta fayl bering vaishchilarni 1, 2, 4, 8, 16 qilib o'lchang. Qaysi qiymatda to'yinish boshlanadi?asynciobilanhammasining muqobilini yozing (aiofilesyokirun_in_executor). Kod soddaroq bo'ldimi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.