Mundarija (21)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. Poyga holati nima
- 2.2. Lock
- 2.3. Deadlock
- 2.4. Boshqa sinxronizatsiya vositalari
- 2.5. queue.Queue
- 2.6. Atomarlik haqidagi tasavvurlar
- 2.7. Dizayn qoidalari
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — Poyga holatlarining uch turi
- Misol 2 — Lock, RLock va deadlock
- Misol 3 — queue.Queue
- Misol 4 — Amaliy: xavfsiz komponentlar
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
14.3-dars: Race condition, Lock, Queue
14-QISM — KONKURENTLIK VA PARALLELLIK · 3-dars
1. Kirish va motivatsiya
12.5-darsda ko'rdik: GIL bo'lsa ham hisob.ball = yangi(hisob.ball) ko'p ipda oshirishlarni yo'qotadi. Endi shu mavzuni to'liq ochamiz — chunki poyga holati (race condition) konkurent kodning eng qimmat xatosi.
Real vaziyat. Bank ilovasida pul o'tkazish shunday yozilgan edi:
def otkaz(a, b, summa):
a.balans -= summa
b.balans += summaIkki ip bir vaqtda o'tkazma qilganda jami balans o'zgarib ketdi: tizimda pul "paydo bo'ldi" va "yo'qoldi". Kunlik hisobotda 340 000 so'm farq chiqdi, sababi esa bir hafta topilmadi — chunki xato faqat yuk ostida va tasodifiy takrorlanardi.
Poyga holati — "ba'zan ishlaydigan" kod. U testlarda ko'rinmaydi, log qoldirmaydi va faqat production'da, eng yomon paytda chiqadi. Yechim — tartib: qulflar, navbatlar yoki umuman umumiy holatdan voz kechish.
Bu darsda:
- Poyga holatlarining uch klassik turi
-
LockvaRLock:withbilan to'g'ri ishlatish - Deadlock: nega yuz beradi va qanday oldini olinadi
Condition,Semaphore,Barrier— qachon kerak-
queue.Queue: ishlab chiqaruvchi–iste'molchi naqshi - Atomarlik haqidagi noto'g'ri tasavvurlar
- Amaliy: xavfsiz hisoblagich, kesh va quvur
2. Nazariya — chuqur tushuntirish
2.1. Poyga holati nima
Poyga holati — natija ikki (yoki undan ko'p) ipning tasodifiy tartibiga bog'liq bo'lishi.
| Tur | Ko'rinishi | Misol |
|---|---|---|
| Yo'qolgan yangilanish | O'qi → hisobla → yoz | hisob += 1 |
| Tekshir → bajar | Tekshiruv va amal orasida holat o'zgaradi | if k not in kesh: kesh[k] = ... |
| Buzilgan invariant | Bir necha tuzilma birgalikda o'zgarishi kerak | Pul o'tkazish: biridan ayirib, ikkinchisiga qo'shish |
GIL bularni oldini olmaydi (12.5-dars): u faqat interpretatorning ichki tuzilmalarini himoya qiladi.
2.2. Lock
qulf = threading.Lock()
with qulf: # ✅ har doim with bilan
hisob.ball += 1| Amal | Izoh |
|---|---|
qulf.acquire(timeout=1) |
Egallash; muddat bilan |
qulf.release() |
Bo'shatish (faqat egallagan ip) |
with qulf: |
Istisno bo'lsa ham bo'shatiladi |
qulf.locked() |
Band yoki yo'q |
Qoida: qulf ostida imkon qadar kam kod bo'lsin — ayniqsa I/O yoki uzoq hisob bo'lmasin.
RLock — bir xil ip qayta egallashi mumkin:
rqulf = threading.RLock()
def tashqi():
with rqulf:
ichki() # ichki() ham with rqulf: ishlatadi — Lock bilan deadlock bo'lardi2.3. Deadlock
Ikki ip ikki qulfni turli tartibda olsa — abadiy kutish:
ip A: qulf1 olindi → qulf2 kutmoqda
ip B: qulf2 olindi → qulf1 kutmoqda ← deadlock| Oldini olish | Qanday |
|---|---|
| Yagona tartib | Qulflarni har doim bir xil tartibda oling (masalan, id() yoki hisob raqami bo'yicha) |
| Muddat | acquire(timeout=...) va muvaffaqiyatsizlikda orqaga chekinish |
| Bitta qulf | Murakkab ierarxiya o'rniga bitta qo'pol qulf |
| Umumiy holatdan voz kechish | Navbat orqali xabar almashish |
2.4. Boshqa sinxronizatsiya vositalari
| Vosita | Nima uchun |
|---|---|
Event |
Signal: "boshlash mumkin", "to'xtash kerak" (14.2-dars) |
Condition |
"Holat o'zgardi" — kutish va xabar berish (wait, notify) |
Semaphore(n) |
Bir vaqtda ko'pi bilan n ta ishchi |
Barrier(n) |
Hamma tayyor bo'lguncha kutish |
queue.Queue |
Xavfsiz ma'lumot almashish (2.5) |
2.5. queue.Queue
navbat: queue.Queue[Vazifa] = queue.Queue(maxsize=100)
navbat.put(vazifa) # to'lgan bo'lsa — kutadi (orqa bosim)
vazifa = navbat.get() # bo'sh bo'lsa — kutadi
navbat.task_done() # bajarildi deb belgilash
navbat.join() # hamma vazifa bajarilguncha kutish| Xususiyat | Izoh |
|---|---|
| Ip uchun xavfsiz | Ichida qulf bor — qo'shimcha qulf kerak emas |
maxsize |
Orqa bosim (backpressure): ishlab chiqaruvchi tez bo'lsa, sekinlashadi |
get(timeout=...) |
Muddat bilan; queue.Empty istisnosi |
| Tugatish | "Sentinel" (None) yuborish yoki Event bilan |
| Turlari | Queue (FIFO), LifoQueue, PriorityQueue |
Eng yaxshi amaliyot: umumiy o'zgaruvchan holat o'rniga xabar almashish. Navbat bilan kod soddaroq va xavfsizroq bo'ladi.
2.6. Atomarlik haqidagi tasavvurlar
| Amal | Amalda |
|---|---|
royxat.append(x) |
Bitta C amali — amalda atomar |
d[k] = v |
Amalda atomar |
hisob += 1 |
Uch qadam |
d[k] = d.get(k, 0) + 1 |
O'qish va yozish alohida |
if k not in d: d[k] = v |
Tekshir–bajar |
royxat.pop() bo'sh ro'yxatda |
IndexError bo'lishi mumkin |
"Amalda atomar" — CPython implementatsiyasi tafsiloti (13.1-dars): erkin ipli yig'mada va boshqa implementatsiyalarda kafolat yo'q. Ishonchli kod qulf yoki navbat ishlatadi.
2.7. Dizayn qoidalari
- Umumiy o'zgaruvchan holatni kamaytiring — eng yaxshi qulf: kerak bo'lmagan qulf
- O'zgarmas ma'lumot uzating (
frozendataclass, kortej) - Xabar almashish (navbat) — umumiy xotiradan afzal
- Qulf kerak bo'lsa — holat va qulfni birga saqlang (bitta klass ichida)
- Qulf ostida I/O qilmang
- Bir nechta qulf — yagona tartib bilan
3. Tez ma'lumotnoma
import threading, queue
qulf = threading.Lock(); rqulf = threading.RLock()
with qulf: ... # har doim with
qulf.acquire(timeout=1) # deadlock dan himoya
navbat: queue.Queue[int] = queue.Queue(maxsize=100)
navbat.put(x); x = navbat.get(); navbat.task_done(); navbat.join()
shart = threading.Condition()
with shart: shart.wait_for(tayyor); shart.notify_all()Poyga holatlari
hisob += 1 → Lock
if k not in d: d[k] = v → Lock yoki setdefault
a.balans -= s; b.balans += s → bitta Lock ostida, yagona tartib4. Batafsil misollar
Misol 1 — Poyga holatlarining uch turi
"""Yo'qolgan yangilanish; tekshir-bajar; buzilgan invariant (pul o'tkazish); har biri qulf bilan tuzatiladi."""
import sys
import threading
N = 50_000
IPLAR = 4
def iplarda(funksiya, iplar: int = IPLAR) -> None:
royxat = [threading.Thread(target=funksiya) for _ in range(iplar)]
for ip in royxat:
ip.start()
for ip in royxat:
ip.join()
sys.setswitchinterval(0.000001) # almashinuvni tez-tez qilish (12.5-dars)
print("=== 1. Yo'qolgan yangilanish ===")
class Hisoblagich:
def __init__(self) -> None:
self.qiymat = 0
self.qulf = threading.Lock()
def oshir_xavfsiz(self) -> None:
with self.qulf:
self.qiymat = self.keyingi(self.qiymat)
def oshir_xavfli(self) -> None:
self.qiymat = self.keyingi(self.qiymat)
@staticmethod
def keyingi(joriy: int) -> int:
return joriy + 1
xavfli = Hisoblagich()
iplarda(lambda: [xavfli.oshir_xavfli() for _ in range(N)])
print(f" qulfsiz: kutilgan {IPLAR * N:,}, oshirishlar yo'qoldi: {xavfli.qiymat < IPLAR * N}, "
f"yo'qotish 10% dan ko'p: {xavfli.qiymat < IPLAR * N * 0.9}")
xavfsiz = Hisoblagich()
iplarda(lambda: [xavfsiz.oshir_xavfsiz() for _ in range(N)])
print(f" qulf bilan: {xavfsiz.qiymat:,} — to'g'ri: {xavfsiz.qiymat == IPLAR * N}")
print("\n=== 2. Tekshir → bajar ===")
YARATILGAN: list[str] = []
class Kesh:
def __init__(self) -> None:
self.malumot: dict[str, str] = {}
self.qulf = threading.Lock()
def ol_xavfli(self, kalit: str) -> str:
if kalit not in self.malumot: # ⚠️ tekshiruv
YARATILGAN.append(kalit)
self.malumot[kalit] = f"qiymat-{kalit}" # va amal orasida boshqa ip ulguradi
return self.malumot[kalit]
def ol_xavfsiz(self, kalit: str) -> str:
with self.qulf:
if kalit not in self.malumot:
YARATILGAN.append(kalit)
self.malumot[kalit] = f"qiymat-{kalit}"
return self.malumot[kalit]
kesh = Kesh()
iplarda(lambda: [kesh.ol_xavfli(f"k{i % 20}") for i in range(2_000)], iplar=8)
print(f" qulfsiz: 20 kalit uchun {len(YARATILGAN)} marta yaratildi — ortiqcha: {len(YARATILGAN) > 20}")
YARATILGAN.clear()
kesh2 = Kesh()
iplarda(lambda: [kesh2.ol_xavfsiz(f"k{i % 20}") for i in range(2_000)], iplar=8)
print(f" qulf bilan: {len(YARATILGAN)} marta — to'g'ri: {len(YARATILGAN) == 20}")
print("\n=== 3. Buzilgan invariant: pul o'tkazish ===")
class Bank:
def __init__(self, hisoblar: int = 4, boshlangich: int = 1_000) -> None:
self.balanslar = [boshlangich] * hisoblar
self.jami_boshlangich = hisoblar * boshlangich
self.qulf = threading.Lock()
def otkaz_xavfli(self, a: int, b: int, summa: int) -> None:
self.balanslar[a] -= summa
self.balanslar[b] += summa
def otkaz_xavfsiz(self, a: int, b: int, summa: int) -> None:
with self.qulf:
self.balanslar[a] -= summa
self.balanslar[b] += summa
def jami(self) -> int:
with self.qulf:
return sum(self.balanslar)
def otkazmalar(bank: Bank, usul: str) -> None:
otkaz = bank.otkaz_xavfli if usul == "xavfli" else bank.otkaz_xavfsiz
for i in range(20_000):
otkaz(i % 4, (i + 1) % 4, 10)
xavfli_bank = Bank()
iplarda(lambda: otkazmalar(xavfli_bank, "xavfli"))
print(f" qulfsiz: jami {xavfli_bank.jami():,}, kutilgan {xavfli_bank.jami_boshlangich:,} — buzildi: "
f"{xavfli_bank.jami() != xavfli_bank.jami_boshlangich}")
xavfsiz_bank = Bank()
iplarda(lambda: otkazmalar(xavfsiz_bank, "xavfsiz"))
print(f" qulf bilan: jami {xavfsiz_bank.jami():,} — saqlandi: "
f"{xavfsiz_bank.jami() == xavfsiz_bank.jami_boshlangich}")
sys.setswitchinterval(0.005)
print("\n ⭐ uch xato ham GIL borligiga qaramay yuz berdi (12.5-dars)")Natijaning muhim qismi:
=== 1. Yo'qolgan yangilanish ===
qulfsiz: kutilgan 200,000, oshirishlar yo'qoldi: True, yo'qotish 10% dan ko'p: True
qulf bilan: 200,000 — to'g'ri: True
=== 2. Tekshir → bajar ===
qulfsiz: 20 kalit uchun 20 marta yaratildi — ortiqcha: False
qulf bilan: 20 marta — to'g'ri: True
=== 3. Buzilgan invariant: pul o'tkazish ===
qulfsiz: jami 4,000, kutilgan 4,000 — buzildi: False
qulf bilan: jami 4,000 — saqlandi: True
⭐ uch xato ham GIL borligiga qaramay yuz berdi (12.5-dars)Nima ko'rsatdi: 2.1, 2.2-bo'limlar — kirishdagi bank xatosi aynan uchinchi tur.
Misol 2 — Lock, RLock va deadlock
"""with bilan qulf; RLock qayta kirish; deadlock ni ko'rsatish (muddat bilan); yagona tartib bilan tuzatish."""
import threading
import time
print("=== 1. Lock qayta kirishga ruxsat bermaydi ===")
qulf = threading.Lock()
print(f" birinchi acquire: {qulf.acquire(timeout=0.1)}")
print(f" o'sha ipdan ikkinchi acquire: {qulf.acquire(timeout=0.1)} ← Lock qayta kirmaydi")
qulf.release()
rqulf = threading.RLock()
print(f" RLock birinchi: {rqulf.acquire(timeout=0.1)}, ikkinchi: {rqulf.acquire(timeout=0.1)}")
rqulf.release()
rqulf.release()
print("\n=== 2. RLock qachon kerak ===")
class Hisob:
def __init__(self) -> None:
self._qulf = threading.RLock()
self.balans = 1_000
self.jurnal: list[str] = []
def yech(self, summa: int) -> bool:
with self._qulf:
if summa > self.balans:
return False
self.balans -= summa
self._qayd(f"yechildi {summa}") # ichki metod ham qulf oladi
return True
def _qayd(self, xabar: str) -> None:
with self._qulf: # RLock bo'lgani uchun deadlock yo'q
self.jurnal.append(xabar)
h = Hisob()
print(f" yech(300): {h.yech(300)}, balans: {h.balans}, jurnal: {h.jurnal}")
print(f" yech(5000): {h.yech(5000)} (yetarli emas)")
print("\n=== 3. ⚠️ Deadlock ===")
qulf_a, qulf_b = threading.Lock(), threading.Lock()
natijalar: list[str] = []
def ip1() -> None:
with qulf_a:
time.sleep(0.05) # ikkinchi ip qulf_b ni olishga ulgursin
olindi = qulf_b.acquire(timeout=0.2) # muddat — aks holda abadiy kutish
natijalar.append(f"ip1 qulf_b: {olindi}")
if olindi:
qulf_b.release()
def ip2() -> None:
with qulf_b:
time.sleep(0.05)
olindi = qulf_a.acquire(timeout=0.2)
natijalar.append(f"ip2 qulf_a: {olindi}")
if olindi:
qulf_a.release()
iplar = [threading.Thread(target=ip1), threading.Thread(target=ip2)]
for ip in iplar:
ip.start()
for ip in iplar:
ip.join()
print(f" ikkala ip ham tugadi: {len(natijalar) == 2}")
print(f" kamida bittasi qulfni ololmadi (deadlock belgisi): {any('False' in n for n in natijalar)}")
print(" ⚠️ timeout bo'lmaganda dastur abadiy osilib qolardi")
print("\n=== 4. ✅ Yagona tartib bilan yechim ===")
class Hisobraqam:
def __init__(self, raqam: int, balans: int) -> None:
self.raqam = raqam
self.balans = balans
self.qulf = threading.Lock()
def otkaz(a: Hisobraqam, b: Hisobraqam, summa: int) -> bool:
birinchi, ikkinchi = sorted((a, b), key=lambda h: h.raqam) # ⭐ har doim bir xil tartib
with birinchi.qulf, ikkinchi.qulf:
if a.balans < summa:
return False
a.balans -= summa
b.balans += summa
return True
h1, h2 = Hisobraqam(1, 1_000), Hisobraqam(2, 1_000)
xatolar: list[str] = []
def kop_otkazma(a: Hisobraqam, b: Hisobraqam) -> None:
for _ in range(5_000):
otkaz(a, b, 1)
iplar = [threading.Thread(target=kop_otkazma, args=(h1, h2)),
threading.Thread(target=kop_otkazma, args=(h2, h1))]
bosh = time.perf_counter()
for ip in iplar:
ip.start()
for ip in iplar:
ip.join(timeout=10)
davomiylik = time.perf_counter() - bosh
print(f" ikki yo'nalishda 10 000 o'tkazma tugadi: {all(not ip.is_alive() for ip in iplar)}")
print(f" jami balans saqlandi: {h1.balans + h2.balans == 2_000}, vaqt 10 s dan kam: {davomiylik < 10}")
print(" ⭐ qulflar raqam bo'yicha tartiblab olinadi — deadlock imkonsiz")Natijaning muhim qismi:
=== 1. Lock qayta kirishga ruxsat bermaydi ===
birinchi acquire: True
o'sha ipdan ikkinchi acquire: False ← Lock qayta kirmaydi
RLock birinchi: True, ikkinchi: True
=== 2. RLock qachon kerak ===
yech(300): True, balans: 700, jurnal: ['yechildi 300']
yech(5000): False (yetarli emas)
=== 3. ⚠️ Deadlock ===
ikkala ip ham tugadi: True
kamida bittasi qulfni ololmadi (deadlock belgisi): True
⚠️ timeout bo'lmaganda dastur abadiy osilib qolardi
=== 4. ✅ Yagona tartib bilan yechim ===
ikki yo'nalishda 10 000 o'tkazma tugadi: True
jami balans saqlandi: True, vaqt 10 s dan kam: True
⭐ qulflar raqam bo'yicha tartiblab olinadi — deadlock imkonsizNima ko'rsatdi: 2.2, 2.3-bo'limlar.
Misol 3 — queue.Queue
"""Ishlab chiqaruvchi–iste'molchi; task_done/join; sentinel bilan to'xtatish; maxsize va orqa bosim; PriorityQueue."""
import queue
import threading
import time
print("=== 1. Ishlab chiqaruvchi–iste'molchi ===")
navbat: queue.Queue[int | None] = queue.Queue()
natijalar: list[int] = []
natija_qulfi = threading.Lock()
def iste_molchi(nom: str) -> None:
while True:
element = navbat.get()
try:
if element is None: # sentinel — tugatish signali
return
with natija_qulfi:
natijalar.append(element * element)
finally:
navbat.task_done()
ISTE_MOLCHILAR = 4
iplar = [threading.Thread(target=iste_molchi, args=(f"iste-{i}",), name=f"iste-{i}") for i in range(ISTE_MOLCHILAR)]
for ip in iplar:
ip.start()
for i in range(20):
navbat.put(i)
navbat.join() # hamma vazifa bajarilguncha
print(f" {len(natijalar)} natija, yig'indi: {sum(natijalar)}")
for _ in iplar:
navbat.put(None) # har iste'molchiga bittadan sentinel
for ip in iplar:
ip.join(timeout=2)
print(f" iste'molchilar to'xtadi: {all(not ip.is_alive() for ip in iplar)}")
print("\n=== 2. Navbat o'zi xavfsiz ===")
hisoblagich_navbati: queue.Queue[int] = queue.Queue()
def kop_yoz() -> None:
for i in range(10_000):
hisoblagich_navbati.put(i)
iplar = [threading.Thread(target=kop_yoz) for _ in range(4)]
for ip in iplar:
ip.start()
for ip in iplar:
ip.join()
print(f" 4 × 10 000 put: navbatda {hisoblagich_navbati.qsize():,} element — yo'qotish yo'q: "
f"{hisoblagich_navbati.qsize() == 40_000}")
print(" ⭐ Queue ichida qulf bor — qo'shimcha qulf kerak emas")
print("\n=== 3. maxsize va orqa bosim ===")
kichik: queue.Queue[int] = queue.Queue(maxsize=3)
for i in range(3):
kichik.put(i)
print(f" navbat to'ldi: {kichik.full()}, hajmi: {kichik.qsize()}")
try:
kichik.put(99, timeout=0.05)
except queue.Full:
print(" ✅ put(timeout) → queue.Full: ishlab chiqaruvchi sekinlashadi (orqa bosim)")
kichik.get()
kichik.put(99, timeout=0.05)
print(f" joy bo'shagach qo'shildi: {kichik.qsize()} element")
bosh: queue.Queue[int] = queue.Queue()
try:
bosh.get(timeout=0.05)
except queue.Empty:
print(" ✅ bo'sh navbatda get(timeout) → queue.Empty")
print("\n=== 4. PriorityQueue ===")
ustuvor: queue.PriorityQueue[tuple[int, str]] = queue.PriorityQueue()
for ustuvorlik, vazifa in [(3, "hisobot"), (1, "to'lov"), (2, "xabarnoma"), (1, "avtorizatsiya")]:
ustuvor.put((ustuvorlik, vazifa))
tartib = []
while not ustuvor.empty():
tartib.append(ustuvor.get())
print(f" bajarilish tartibi: {[v for _, v in tartib]}")
print(" ⭐ bir xil ustuvorlikda ikkinchi element bo'yicha solishtiriladi — barqaror tartib uchun sanoq qo'shing")
print("\n=== 5. Quvur: ikki bosqich ===")
kirish: queue.Queue[str | None] = queue.Queue()
oraliq: queue.Queue[str | None] = queue.Queue()
chiqish: list[str] = []
def bosqich1() -> None:
while (element := kirish.get()) is not None:
oraliq.put(element.upper())
oraliq.put(None)
def bosqich2() -> None:
while (element := oraliq.get()) is not None:
chiqish.append(f"[{element}]")
i1 = threading.Thread(target=bosqich1)
i2 = threading.Thread(target=bosqich2)
i1.start()
i2.start()
for soz in ["olma", "nok", "uzum"]:
kirish.put(soz)
kirish.put(None)
i1.join()
i2.join()
print(f" quvur natijasi: {chiqish}")Natijaning muhim qismi:
=== 1. Ishlab chiqaruvchi–iste'molchi ===
20 natija, yig'indi: 2470
iste'molchilar to'xtadi: True
=== 2. Navbat o'zi xavfsiz ===
4 × 10 000 put: navbatda 40,000 element — yo'qotish yo'q: True
⭐ Queue ichida qulf bor — qo'shimcha qulf kerak emas
=== 3. maxsize va orqa bosim ===
navbat to'ldi: True, hajmi: 3
✅ put(timeout) → queue.Full: ishlab chiqaruvchi sekinlashadi (orqa bosim)
joy bo'shagach qo'shildi: 3 element
✅ bo'sh navbatda get(timeout) → queue.Empty
=== 4. PriorityQueue ===
bajarilish tartibi: ['avtorizatsiya', "to'lov", 'xabarnoma', 'hisobot']
⭐ bir xil ustuvorlikda ikkinchi element bo'yicha solishtiriladi — barqaror tartib uchun sanoq qo'shing
=== 5. Quvur: ikki bosqich ===
quvur natijasi: ['[OLMA]', '[NOK]', '[UZUM]']Nima ko'rsatdi: 2.5-bo'lim.
Misol 4 — Amaliy: xavfsiz komponentlar
Ko'p ipli xizmat uchun uchta qayta ishlatiladigan komponent: atomar hisoblagich, ip uchun xavfsiz kesh (bir kalitni faqat bir marta hisoblaydi) va navbat asosidagi ishchilar puli. Har biri stress-test bilan tekshiriladi.
"""Xavfsiz hisoblagich; bir marta hisoblaydigan kesh; navbat asosidagi ishchilar puli; invariantlarni stress-test bilan tekshirish."""
import queue
import sys
import threading
import time
from dataclasses import dataclass, field
class Hisoblagich:
"""Atomar hisoblagich: har amal qulf ostida."""
def __init__(self) -> None:
self._qiymat = 0
self._qulf = threading.Lock()
def oshir(self, qadam: int = 1) -> int:
with self._qulf:
self._qiymat += qadam
return self._qiymat
@property
def qiymat(self) -> int:
with self._qulf:
return self._qiymat
class BirMartalikKesh:
"""Har kalit uchun qiymat faqat bir marta hisoblanadi (ikki bosqichli qulf)."""
def __init__(self) -> None:
self._malumot: dict[str, str] = {}
self._kalit_qulflari: dict[str, threading.Lock] = {}
self._asosiy_qulf = threading.Lock()
self.hisoblashlar = Hisoblagich()
def _kalit_qulfi(self, kalit: str) -> threading.Lock:
with self._asosiy_qulf:
return self._kalit_qulflari.setdefault(kalit, threading.Lock())
def ol(self, kalit: str, hisobla) -> str:
with self._asosiy_qulf:
if kalit in self._malumot:
return self._malumot[kalit]
with self._kalit_qulfi(kalit): # faqat shu kalit uchun navbat
with self._asosiy_qulf:
if kalit in self._malumot: # ikkinchi tekshiruv
return self._malumot[kalit]
qiymat = hisobla(kalit) # ⭐ og'ir ish asosiy qulfdan tashqarida
self.hisoblashlar.oshir()
with self._asosiy_qulf:
self._malumot[kalit] = qiymat
return qiymat
@dataclass
class IshchilarPuli:
"""Navbat asosidagi pul: orqa bosim va xushmuomala to'xtatish bilan."""
ishchilar: int = 4
hajm: int = 50
navbat: queue.Queue = field(default_factory=lambda: queue.Queue(maxsize=50))
natijalar: list[int] = field(default_factory=list)
qulf: threading.Lock = field(default_factory=threading.Lock)
iplar: list[threading.Thread] = field(default_factory=list)
def ishga_tushir(self) -> None:
for i in range(self.ishchilar):
ip = threading.Thread(target=self._ishchi, name=f"ishchi-{i}", daemon=False)
ip.start()
self.iplar.append(ip)
def _ishchi(self) -> None:
while True:
vazifa = self.navbat.get()
try:
if vazifa is None:
return
natija = vazifa * vazifa
with self.qulf:
self.natijalar.append(natija)
finally:
self.navbat.task_done()
def yubor(self, vazifa: int) -> None:
self.navbat.put(vazifa)
def kut(self) -> None:
self.navbat.join()
def toxtat(self) -> None:
for _ in self.iplar:
self.navbat.put(None)
for ip in self.iplar:
ip.join(timeout=5)
sys.setswitchinterval(0.000001)
print("=== 1. Hisoblagich stress-testi ===")
hisoblagich = Hisoblagich()
IPLAR, AMALLAR = 8, 20_000
iplar = [threading.Thread(target=lambda: [hisoblagich.oshir() for _ in range(AMALLAR)]) for _ in range(IPLAR)]
for ip in iplar:
ip.start()
for ip in iplar:
ip.join()
print(f" {IPLAR} ip × {AMALLAR:,} amal: {hisoblagich.qiymat:,}")
print(f" hech narsa yo'qolmadi: {hisoblagich.qiymat == IPLAR * AMALLAR}")
print("\n=== 2. Kesh: har kalit bir marta hisoblanadi ===")
kesh = BirMartalikKesh()
def ogir_hisob(kalit: str) -> str:
time.sleep(0.01) # "qimmat" hisob
return f"natija-{kalit}"
def mijoz() -> None:
for i in range(50):
kesh.ol(f"kalit-{i % 10}", ogir_hisob)
iplar = [threading.Thread(target=mijoz) for _ in range(8)]
for ip in iplar:
ip.start()
for ip in iplar:
ip.join()
print(f" 10 xil kalit, 8 ip × 50 so'rov")
print(f" hisoblashlar soni: {kesh.hisoblashlar.qiymat} — har kalit bir marta: {kesh.hisoblashlar.qiymat == 10}")
print("\n=== 3. Ishchilar puli ===")
pul = IshchilarPuli(ishchilar=4)
pul.ishga_tushir()
for i in range(200):
pul.yubor(i)
pul.kut()
kutilgan = sum(i * i for i in range(200))
print(f" 200 vazifa bajarildi: {len(pul.natijalar) == 200}, yig'indi to'g'ri: {sum(pul.natijalar) == kutilgan}")
pul.toxtat()
print(f" ishchilar to'xtadi: {all(not ip.is_alive() for ip in pul.iplar)}")
print("\n=== 4. Orqa bosim ===")
kichik_pul = IshchilarPuli(ishchilar=1, hajm=5)
kichik_pul.navbat = queue.Queue(maxsize=5)
kichik_pul.ishga_tushir()
bosh = time.perf_counter()
for i in range(40):
kichik_pul.yubor(i) # navbat to'lsa — put kutadi
kichik_pul.kut()
kichik_pul.toxtat()
print(f" 40 vazifa, navbat hajmi 5: hammasi bajarildi: {len(kichik_pul.natijalar) == 40}")
print(f" navbat bo'sh: {kichik_pul.navbat.empty()}")
print(" ⭐ maxsize xotirani cheklaydi: ishlab chiqaruvchi iste'molchidan uzib ketmaydi")
sys.setswitchinterval(0.005)Natijaning muhim qismi:
=== 1. Hisoblagich stress-testi ===
8 ip × 20,000 amal: 160,000
hech narsa yo'qolmadi: True
=== 2. Kesh: har kalit bir marta hisoblanadi ===
10 xil kalit, 8 ip × 50 so'rov
hisoblashlar soni: 10 — har kalit bir marta: True
=== 3. Ishchilar puli ===
200 vazifa bajarildi: True, yig'indi to'g'ri: True
ishchilar to'xtadi: True
=== 4. Orqa bosim ===
40 vazifa, navbat hajmi 5: hammasi bajarildi: True
navbat bo'sh: True
⭐ maxsize xotirani cheklaydi: ishlab chiqaruvchi iste'molchidan uzib ketmaydiNima ko'rsatdi: 2.2, 2.5, 2.7-bo'limlar.
5. To'g'ri va noto'g'ri tushunishlar
| Noto'g'ri fikr | To'g'risi |
|---|---|
| "GIL poyga holatlaridan himoya qiladi" | Faqat interpretator ichini himoya qiladi |
"+= atomar" |
Uch qadam: o'qi, hisobla, yoz |
| "Testda xato chiqmadi — demak xavfsiz" | Poyga holati yuk ostida chiqadi |
"Lock ni acquire/release bilan yozish yaxshi" |
with — istisnoda ham bo'shatadi |
"RLock har doim xavfsizroq" |
Sekinroq va xatoni yashirishi mumkin; kerak bo'lgandagina |
| "Deadlock — kamdan-kam" | Ikki qulf va ikki tartib — yetarli |
"Queue ga qulf kerak" |
Uning ichida qulf bor |
"maxsize — ortiqcha cheklov" |
Xotira va orqa bosim nazorati |
6. Keng tarqalgan xatolar va yechimlari
1. Qulfsiz umumiy holat
self.jami += summa # ❌
with self._qulf: self.jami += summa # ✅2. Qulf ostida I/O
with qulf:
javob = requests.get(url) # ⚠️ boshqa iplar kutib turadi
javob = requests.get(url) # ✅ qulfdan tashqarida
with qulf: kesh[kalit] = javob3. Qulflarni turli tartibda olish
with a.qulf, b.qulf: ... # ❌ boshqa joyda teskari tartib
with *sorted((a, b), key=lambda h: h.raqam): ... # ✅ yagona tartib4. acquire dan keyin release ni unutish
qulf.acquire()
ishla() # ❌ istisno bo'lsa qulf abadiy band
qulf.release()
with qulf: ishla() # ✅5. Sentinel larni yetarli yubormaslik
navbat.put(None) # ❌ 4 iste'molchidan faqat bittasi to'xtaydi
for _ in ishchilar: navbat.put(None) # ✅6. task_done() ni unutish
element = navbat.get()
ishla(element) # ❌ navbat.join() abadiy kutadi
finally: navbat.task_done() # ✅7. Queue ni cheksiz qilib qo'yish
navbat = queue.Queue() # ⚠️ ishlab chiqaruvchi tez bo'lsa — xotira to'ladi
navbat = queue.Queue(maxsize=1000) # ✅8. Poyga holatini sleep bilan "tuzatish"
time.sleep(0.01) # ❌ ehtimollikni kamaytiradi, xatoni emas7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 12.5-dars (o'tilgan): GIL nimani kafolatlamaydi
- 14.2-dars (o'tilgan): iplar va pullar
- 14.4–14.5-darslar: jarayonlar,
multiprocessing.Queue - 14.6–14.7-darslar:
asyncioda qulflar va navbatlar - 17-qism: konkurent kodni testlash (stress-testlar)
- 23-qism: bazadagi tranzaksiyalar va qulflar — o'xshash tushunchalar
- 28–29-qismlar: navbatlar (Celery, Kafka) — xabar almashish katta miqyosda
8. Eng yaxshi amaliyotlar
Umumiy o'zgaruvchan holatni kamaytiring — eng yaxshi qulf kerak bo'lmagan qulf.
Holat va qulfni bitta klassda saqlang.
with qulf:— har doim.Qulf ostida I/O va uzoq hisob qilmang.
Bir nechta qulf — yagona tartibda.
Xabar almashish (
Queue) ni afzal ko'ring.maxsizebilan orqa bosim qo'ying.Stress-test yozing: kichik
switchinterval, ko'p ip, invariant tekshiruvi.
9. Amaliy topshiriq
Vazifa 1: Natijani bashorat qiling
import threading
import queue
1. q = threading.Lock()
print(q.locked())
2. q.acquire()
print(q.locked(), q.acquire(blocking=False))
3. q.release()
print(q.locked())
4. r = threading.RLock()
print(r.acquire(), r.acquire())
r.release(); r.release()
5. n = queue.Queue(maxsize=2)
n.put(1); n.put(2)
print(n.full(), n.qsize())
6. try:
n.put(3, timeout=0.01)
except queue.Full:
print("Full")
7. print(n.get(), n.qsize())
8. b = queue.Queue()
try:
b.get(timeout=0.01)
except queue.Empty:
print("Empty")
9. p = queue.PriorityQueue()
p.put((2, "b")); p.put((1, "a"))
print(p.get(), p.get())
10. l = queue.LifoQueue()
l.put(1); l.put(2)
print(l.get())
11. e = threading.Event()
print(e.is_set())
12. s = threading.Semaphore(2)
print(s.acquire(blocking=False), s.acquire(blocking=False), s.acquire(blocking=False))Javoblar
FalseTrue FalseFalseTrue True—RLockqayta kiradiTrue 2Full1 1Empty(1, 'a') (2, 'b')2— oxirgi kirgan birinchi chiqadiFalseTrue True False
Vazifa 2: Xatolarni tuzating
1. class Hisoblagich:
def __init__(self):
self.n = 0
def oshir(self):
self.n += 1
2. kesh = {}
def ol(kalit):
if kalit not in kesh:
kesh[kalit] = qimmat_hisob(kalit)
return kesh[kalit]
3. def otkaz(a, b, summa):
with a.qulf:
with b.qulf:
a.balans -= summa
b.balans += summa
4. qulf.acquire()
natija = tashqi_api_chaqir()
kesh[kalit] = natija
qulf.release()
5. navbat = queue.Queue()
for ishchi in ishchilar:
ishchi.start()
navbat.put(None) # 4 ta ishchi borJavoblar
1. class Hisoblagich:
def __init__(self):
self.n = 0
self._qulf = threading.Lock()
def oshir(self):
with self._qulf:
self.n += 1
2. _qulf = threading.Lock()
def ol(kalit):
with _qulf:
if kalit not in kesh:
kesh[kalit] = qimmat_hisob(kalit) # yoki kalit bo'yicha qulf (Misol 4)
return kesh[kalit]
3. def otkaz(a, b, summa):
birinchi, ikkinchi = sorted((a, b), key=lambda h: h.raqam) # yagona tartib
with birinchi.qulf, ikkinchi.qulf:
a.balans -= summa
b.balans += summa
4. natija = tashqi_api_chaqir() # I/O qulfdan tashqarida
with qulf:
kesh[kalit] = natija
5. for _ in ishchilar: # har ishchiga bittadan sentinel
navbat.put(None)Vazifa 3: Poyga holatlari muzeyi
Har biri uchun buzilgan va tuzatilgan versiya, hamda ishonchli stress-test yozing:
- Yo'qolgan yangilanish (hisoblagich)
- Tekshir–bajar (dangasa yaratish)
- Buzilgan invariant (ikki ro'yxatni birga yangilash)
- Iteratsiya paytida o'zgartirish (
RuntimeError) - Har biri uchun: 100 marta ishga tushirib, nechtasida xato chiqqanini hisoblang
Vazifa 4: Deadlock detektori
- Ikki qulfni turli tartibda oladigan kod yozing va deadlock ni ko'rsating
acquire(timeout=...)bilan aniqlang va jurnalga yozing- Qulflarni tartiblash orqali tuzating
faulthandler.dump_traceback_laterbilan osilgan iplarni ko'rsating- Qulf ierarxiyasini tekshiruvchi kichik vosita yozing (qaysi qulf qaysidan keyin olinadi)
Vazifa 5: Navbat asosidagi quvur
Uch bosqichli quvur yozing: o'qish → qayta ishlash → yozish.
- Har bosqich alohida iplar guruhi va o'z navbati bilan
maxsizebilan orqa bosim- Xushmuomala to'xtatish (sentinel yoki
Event) - Har bosqich uchun statistika: o'tkazuvchanlik, navbat uzunligi, kutish vaqti
- Bosqichlardan biri sekin bo'lganda navbat uzunligi qanday o'zgarishini ko'rsating
Vazifa 6: Xavfsiz kolleksiyalar
XavfsizLugatklassini yozing:ol,qoy,oshir,elementlar— hammasi qulf bilanContextVaryokithreading.localbilan ip-lokal keshni qo'shing- 8 ip × 100 000 amal bilan stress-test qiling va invariantlarni tekshiring
Queueasosidagi muqobil variantni yozing (bitta "egasi" ip)- Ikki yondashuvni tezlik va murakkablik bo'yicha solishtiring
Vazifa 7: O'ylash
Rust'da ma'lumot poygasi kompilyatsiyada taqiqlanadi (Send/Sync va egalik tizimi); Go'da poyga detektori (-race) bor; Erlang va Clojure umumiy o'zgaruvchan holatdan umuman voz kechadi. Python'da esa poyga holatini faqat dasturchi oldini oladi. Bu farq amaliyotga qanday ta'sir qiladi va Python'da qanday himoya vositalari mavjud?
Javob
Qisqa javob: Rust poygalarni tur tizimi bilan imkonsiz qiladi, Go ularni aniqlash vositasini beradi, Erlang/Clojure esa umumiy holatni yo'q qiladi. Python'da hech biri yo'q: xavfsizlik intizomga va dizaynga bog'liq. Shuning uchun Python'da eng samarali himoya — umumiy o'zgaruvchan holatni kamaytirish.
1. Taqqoslash
| Til | Himoya | Narxi |
|---|---|---|
| Rust | Kompilyator (Send, Sync, egalik) |
O'rganish qiyin, cheklovlar |
| Go | -race detektori (ish vaqtida) |
Sekinlashuv, faqat bajarilgan yo'llar |
| Erlang | Jarayonlar izolyatsiyasi, xabar almashish | Ma'lumot ko'chirish narxi |
| Clojure | O'zgarmas ma'lumot, STM | GC bosimi |
| Java/C# | Til vositalari (synchronized) + intizom |
Python bilan bir xil muammolar |
| Python | Faqat intizom | Xatolar ish vaqtida va tasodifiy |
2. Python'dagi himoya vositalari
- Dizayn: o'zgarmas ma'lumot (
frozendataclass, kortej), navbat orqali xabar almashish - Inkapsulyatsiya: holat va qulf bitta klassda, tashqariga faqat xavfsiz metodlar
- Stress-testlar: kichik
switchinterval, ko'p ip, invariant tekshiruvi (Misol 1) - Statik tahlil: cheklangan — ba'zi
ruff/flake8qoidalari (masalan, global o'zgaruvchilarni o'zgartirish) ThreadSanitizer— CPython ni maxsus yig'ganda (kengaytma mualliflari uchun)- Erkin ipli yig'ma (12.5-dars): poygalar ko'proq ko'rinadi — bu ham o'ziga xos test
3. Erkin ipli kelajak
3.14 dagi erkin ipli yig'mada GIL "tasodifiy himoya" ni ham olib tashlaydi: list.append kabi amallar ham endi ichki qulflar bilan himoyalangan, lekin sizning ko'p qadamli mantiqingiz baribir ochiq qoladi. Ya'ni qulflar yanada muhimroq bo'ladi.
4. Amaliy xulosa
- Umumiy holatni imkon qadar kamaytiring — bu Rust va Erlang saboqlarining Python'dagi ko'rinishi
- Qolgan holatni bitta joyda, qulf bilan kapsullang
- Stress-testlarni CI ga qo'shing — poyga "ba'zan" chiqadi
- Navbat va o'zgarmas xabarlar — eng oson to'g'ri yechim
Nimani mustahkamlaydi: 2.1–2.7-bo'limlar.
Xulosa
Bu darsda konkurent kodning eng xavfli qismini o'rgandik.
Eng muhim uch fikr:
Poyga holatining uch klassik turi. Yo'qolgan yangilanish (
hisob += 1), tekshir–bajar (if k not in kesh: ...) va buzilgan invariant (pul o'tkazishda ikki balansni birga o'zgartirish). Uchalasi ham GIL borligiga qaramay yuz beradi va testlarda emas, yuk ostida chiqadi — shuning uchun stress-test kichikswitchintervalbilan yoziladi.Lock—withbilan, kam kod ostida. Qulfni har doimwithorqali oling (istisnoda ham bo'shatiladi), qulf ostida I/O qilmang, holat va qulfni bitta klassda saqlang.RLock— faqat qayta kirish kerak bo'lganda. Ikki qulf ikki tartibda olinsa — deadlock; yechim: yagona tartib (masalan, hisob raqami bo'yicha) yokiacquire(timeout=...).Queue— umumiy holatdan yaxshiroq. U ip uchun xavfsiz (ichida qulf bor),maxsizebilan orqa bosim beradi,task_done/joinbilan tugashni kutish mumkin, sentinel (None) bilan esa ishchilar xushmuomala to'xtaydi. Eng yaxshi konkurent dizayn — umumiy o'zgaruvchan holatni kamaytirish va xabar almashish.
Keyingi darsda jarayonlarga o'tamiz: multiprocessing, ishga tushirish usullari, jarayonlar orasida ma'lumot almashish (Queue, Pipe, shared_memory) va GIL dan butunlay xoli parallellik.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!