IlmHamroh
Python kursi/Konkurentlik va parallellik3/8-dars23 daqiqa
Mundarija (21)

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:

python
def otkaz(a, b, summa):
    a.balans -= summa
    b.balans += summa

Ikki 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
  • Lock va RLock: with bilan 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

python
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:

python
rqulf = threading.RLock()

def tashqi():
    with rqulf:
        ichki()            # ichki() ham with rqulf: ishlatadi — Lock bilan deadlock bo'lardi

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

python
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

  1. Umumiy o'zgaruvchan holatni kamaytiring — eng yaxshi qulf: kerak bo'lmagan qulf
  2. O'zgarmas ma'lumot uzating (frozen dataclass, kortej)
  3. Xabar almashish (navbat) — umumiy xotiradan afzal
  4. Qulf kerak bo'lsa — holat va qulfni birga saqlang (bitta klass ichida)
  5. Qulf ostida I/O qilmang
  6. Bir nechta qulf — yagona tartib bilan

3. Tez ma'lumotnoma

python
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 tartib

4. Batafsil misollar

Misol 1 — Poyga holatlarining uch turi

python
"""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:

text
=== 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

python
"""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:

text
=== 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 imkonsiz

Nima ko'rsatdi: 2.2, 2.3-bo'limlar.

Misol 3 — queue.Queue

python
"""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:

text
=== 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.

python
"""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:

text
=== 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 ketmaydi

Nima 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

python
self.jami += summa                        # ❌
with self._qulf: self.jami += summa       # ✅

2. Qulf ostida I/O

python
with qulf:
    javob = requests.get(url)             # ⚠️ boshqa iplar kutib turadi
javob = requests.get(url)                 # ✅ qulfdan tashqarida
with qulf: kesh[kalit] = javob

3. Qulflarni turli tartibda olish

python
with a.qulf, b.qulf: ...                  # ❌ boshqa joyda teskari tartib
with *sorted((a, b), key=lambda h: h.raqam): ...   # ✅ yagona tartib

4. acquire dan keyin release ni unutish

python
qulf.acquire()
ishla()                                   # ❌ istisno bo'lsa qulf abadiy band
qulf.release()
with qulf: ishla()                        # ✅

5. Sentinel larni yetarli yubormaslik

python
navbat.put(None)                          # ❌ 4 iste'molchidan faqat bittasi to'xtaydi
for _ in ishchilar: navbat.put(None)      # ✅

6. task_done() ni unutish

python
element = navbat.get()
ishla(element)                            # ❌ navbat.join() abadiy kutadi
finally: navbat.task_done()               # ✅

7. Queue ni cheksiz qilib qo'yish

python
navbat = queue.Queue()                    # ⚠️ ishlab chiqaruvchi tez bo'lsa — xotira to'ladi
navbat = queue.Queue(maxsize=1000)        # ✅

8. Poyga holatini sleep bilan "tuzatish"

python
time.sleep(0.01)                          # ❌ ehtimollikni kamaytiradi, xatoni emas

7. 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: asyncio da 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

  1. Umumiy o'zgaruvchan holatni kamaytiring — eng yaxshi qulf kerak bo'lmagan qulf.

  2. Holat va qulfni bitta klassda saqlang.

  3. with qulf: — har doim.

  4. Qulf ostida I/O va uzoq hisob qilmang.

  5. Bir nechta qulf — yagona tartibda.

  6. Xabar almashish (Queue) ni afzal ko'ring.

  7. maxsize bilan orqa bosim qo'ying.

  8. Stress-test yozing: kichik switchinterval, ko'p ip, invariant tekshiruvi.


9. Amaliy topshiriq

Vazifa 1: Natijani bashorat qiling

python
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
  1. False
  2. True False
  3. False
  4. True True — RLock qayta kiradi
  5. True 2
  6. Full
  7. 1 1
  8. Empty
  9. (1, 'a') (2, 'b')
  10. 2 — oxirgi kirgan birinchi chiqadi
  11. False
  12. True True False

Vazifa 2: Xatolarni tuzating

python
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 bor
Javoblar
python
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:

  1. Yo'qolgan yangilanish (hisoblagich)
  2. Tekshir–bajar (dangasa yaratish)
  3. Buzilgan invariant (ikki ro'yxatni birga yangilash)
  4. Iteratsiya paytida o'zgartirish (RuntimeError)
  5. Har biri uchun: 100 marta ishga tushirib, nechtasida xato chiqqanini hisoblang

Vazifa 4: Deadlock detektori

  1. Ikki qulfni turli tartibda oladigan kod yozing va deadlock ni ko'rsating
  2. acquire(timeout=...) bilan aniqlang va jurnalga yozing
  3. Qulflarni tartiblash orqali tuzating
  4. faulthandler.dump_traceback_later bilan osilgan iplarni ko'rsating
  5. 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.

  1. Har bosqich alohida iplar guruhi va o'z navbati bilan
  2. maxsize bilan orqa bosim
  3. Xushmuomala to'xtatish (sentinel yoki Event)
  4. Har bosqich uchun statistika: o'tkazuvchanlik, navbat uzunligi, kutish vaqti
  5. Bosqichlardan biri sekin bo'lganda navbat uzunligi qanday o'zgarishini ko'rsating

Vazifa 6: Xavfsiz kolleksiyalar

  1. XavfsizLugat klassini yozing: ol, qoy, oshir, elementlar — hammasi qulf bilan
  2. ContextVar yoki threading.local bilan ip-lokal keshni qo'shing
  3. 8 ip × 100 000 amal bilan stress-test qiling va invariantlarni tekshiring
  4. Queue asosidagi muqobil variantni yozing (bitta "egasi" ip)
  5. 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

  1. Dizayn: o'zgarmas ma'lumot (frozen dataclass, kortej), navbat orqali xabar almashish
  2. Inkapsulyatsiya: holat va qulf bitta klassda, tashqariga faqat xavfsiz metodlar
  3. Stress-testlar: kichik switchinterval, ko'p ip, invariant tekshiruvi (Misol 1)
  4. Statik tahlil: cheklangan — ba'zi ruff/flake8 qoidalari (masalan, global o'zgaruvchilarni o'zgartirish)
  5. ThreadSanitizer — CPython ni maxsus yig'ganda (kengaytma mualliflari uchun)
  6. 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

  1. Umumiy holatni imkon qadar kamaytiring — bu Rust va Erlang saboqlarining Python'dagi ko'rinishi
  2. Qolgan holatni bitta joyda, qulf bilan kapsullang
  3. Stress-testlarni CI ga qo'shing — poyga "ba'zan" chiqadi
  4. 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:

  1. 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 kichik switchinterval bilan yoziladi.

  2. Lock — with bilan, kam kod ostida. Qulfni har doim with orqali 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) yoki acquire(timeout=...).

  3. Queue — umumiy holatdan yaxshiroq. U ip uchun xavfsiz (ichida qulf bor), maxsize bilan orqa bosim beradi, task_done/join bilan 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.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
14.3-dars: Race condition, Lock, Queue — IlmHamroh