آموزش ماژول threading – کلاس Condition

Please login to bookmark Close

درک عملکرد کلاس Condition

کلاس Condition برای هماهنگی بین تردهایی به کار می‌رود که یکی داده یا شرایطی را «تولید» می‌کند و دیگری منتظر آن است تا «مصرف» کند. فرض کنید یک آشپز (تولیدکننده) و یک گارسون (مصرف‌کننده) داریم. گارسون نمی‌تواند غذا را برای مشتری ببرد مگر اینکه آشپز آن را آماده کرده باشد.

اگر گارسون هر ثانیه به آشپزخانه سر بزند و بپرسد «غذا حاضر است؟ غذا حاضر است؟»، هم خودش خسته می‌شود و هم اعصاب آشپز خرد می‌شود. در کامپیوتر، این کار (که به آن busy-waiting می‌گویند) پردازنده را بی‌جهت درگیر می‌کند و منابع را هدر می‌دهد.

به‌جای این کار از مکانیزم Condition استفاده می‌کنیم: گارسون منتظر می‌ماند و می‌خوابد (بدون مصرف CPU). وقتی غذا حاضر شد، آشپز یک زنگ به صدا درمی‌آورد (notify) تا گارسون بیدار شود و غذا را ببرد.

متدهای اصلی کلاس Condition

متدکار آن
acquire / releaseبستن و بازکردن قفل داخلی. به‌جای این‌ها می‌توان از دستور with استفاده کرد.
waitترد را می‌خواباند و قفل را آزاد می‌کند تا کسی صدایش بزند.
notifyیک تردِ منتظر را بیدار می‌کند.
notify_allاگر چند ترد منتظر باشند، همه را با هم بیدار می‌کند.

نکته‌ی کلیدی که در wait نهفته است: این متد هنگام خوابیدن، قفل را آزاد می‌کند تا ترد دیگر (مثل آشپز) بتواند قفل را بگیرد و کارش را انجام دهد؛ و هنگام بیدارشدن، دوباره قفل را می‌گیرد. همین رفتار است که هماهنگی امن را ممکن می‌کند.

مثال: رستوران

به کد زیر توجه کنید؛ همان رستورانی که در ابتدا توضیح دادیم. یک آشپز داریم که آماده‌سازی سفارش یک ثانیه طول می‌کشد، و یک گارسون که منتظر می‌ماند تا به‌محض آماده‌شدن، سفارش را تحویل مشتری دهد.

import threading
import time

# Create the Condition object
kitchen_condition = threading.Condition()

def waiter():
    print("[Waiter] I am waiting for the pizza to be ready...")

    with kitchen_condition:
        kitchen_condition.wait()
        print("[Waiter] Awesome! The pizza is ready. Serving it to the customer!")

def chef():
    print("[Chef] I am making the pizza. It takes 1 seconds...")
    time.sleep(1)  # Simulating cooking time

    with kitchen_condition:
        print("[Chef] Pizza is done! Ringing the bell...")
        # Wake up the waiting waiter
        kitchen_condition.notify()

# Create threads
waiter_thread = threading.Thread(target=waiter)
chef_thread = threading.Thread(target=chef)

# Start threads
waiter_thread.start()
chef_thread.start()

گام‌به‌گام:

  • گارسون قفل را می‌گیرد و به wait می‌رسد؛ همان‌جا می‌خوابد و قفل را آزاد می‌کند تا آشپز بتواند کارش را ادامه دهد.
  • آشپز پس از یک ثانیه پخت، قفل را می‌گیرد و با notify زنگ را می‌زند.
  • گارسون بیدار می‌شود، قفل را دوباره می‌گیرد و اجرایش را از همان‌جا که خوابیده بود (بعد از wait) ادامه می‌دهد و سفارش را می‌برد.

مشکل بیدارشدن کاذب (Spurious Wakeup)

در دنیای سیستم‌عامل‌ها پدیده‌ای شناخته‌شده به نام «بیدارشدن کاذب» وجود دارد: گاهی سیستم‌عامل (به دلایل فنی مدیریت حافظه و پردازنده) یک تردِ خوابیده را بدون اینکه notify ارسال شده باشد بیدار می‌کند! در این حالت باید بتوانیم تشخیص دهیم که هنوز شرط برقرار نیست و ترد را دوباره بخوابانیم.

راه‌حل استاندارد این است که wait را داخل یک حلقه‌ی while بگذاریم که یک شرط واقعی را بررسی می‌کند. یک متغیر با مقدار اولیه‌ی False می‌سازیم و هنگام notify آن را True می‌کنیم؛ ترد بیدارشده با بررسی همین متغیر می‌فهمد که واقعاً صدا زده شده یا سیستم‌عامل اشتباهی بیدارش کرده است.

import threading
import time

# Create the Condition object
kitchen_condition = threading.Condition()

# A variable to check if food is ready
is_pizza_ready = False

def waiter():
    global is_pizza_ready
    print("[Waiter] I am waiting for the pizza to be ready...")

    with kitchen_condition:
        # Wait until pizza is ready
        while is_pizza_ready == False:
            kitchen_condition.wait()

        print("[Waiter] Awesome! The pizza is ready. Serving it to the customer!")

def chef():
    global is_pizza_ready
    print("[Chef] I am making the pizza. It takes 1 seconds...")
    time.sleep(1)  # Simulating cooking time

    with kitchen_condition:
        is_pizza_ready = True
        print("[Chef] Pizza is done! Ringing the bell...")
        # Wake up the waiting waiter
        kitchen_condition.notify()

# Create threads
waiter_thread = threading.Thread(target=waiter)
chef_thread = threading.Thread(target=chef)

# Start threads
waiter_thread.start()
chef_thread.start()

حالا اگر ترد به‌اشتباه بیدار شود، چون is_pizza_ready هنوز False است، حلقه دوباره wait را اجرا می‌کند و ترد باز می‌خوابد. فقط وقتی آشپز واقعاً متغیر را True کرده باشد، حلقه تمام می‌شود. قاعده‌ی طلایی: همیشه wait را داخل یک حلقه‌ی while که یک شرط را چک می‌کند قرار دهید، نه با if.

مشکل ازدست‌رفتن سیگنال (Missed Notification)

ترتیب مهم است: در حالت عادی باید ابتدا کسی wait کرده باشد و بعد notify ارسال شود. اگر notify زودتر از wait فراخوانی شود، در آن لحظه هیچ‌کس منتظر سیگنال نیست و سیگنال گم می‌شود؛ سپس wait منتظر سیگنالی می‌ماند که دیگر هرگز نمی‌آید و ترد تا ابد می‌خوابد.

import threading
import time

kitchen_condition = threading.Condition()

def waiter():
    # The waiter is busy with something else before going to the kitchen
    print("[Waiter] Doing some other tasks. I will be late for 2 seconds...")
    time.sleep(2)

    print("[Waiter] Now I am waiting for the pizza to be ready...")
    with kitchen_condition:
        kitchen_condition.wait()   # The waiter sleeps here forever!
        print("[Waiter] Awesome! The pizza is ready. Serving it!")

def chef():
    print("[Chef] I am making the pizza. It takes 1 seconds...")
    time.sleep(1)

    with kitchen_condition:
        print("[Chef] Pizza is done! Ringing the bell...")
        kitchen_condition.notify()   # The chef rings the bell and leaves

waiter_thread = threading.Thread(target=waiter)
chef_thread = threading.Thread(target=chef)

waiter_thread.start()
chef_thread.start()

در این کد عمداً کاری کرده‌ایم که notify پیش از wait اجرا شود؛ در نتیجه wait تا ابد منتظر سیگنالی می‌ماند که قبلاً ارسال شده بود. خوشبختانه راه‌حل همان الگوی بخش قبل است: با گذاشتن یک متغیر شرط (مثل is_pizza_ready) و بررسی آن در حلقه‌ی while، ترد بیدارشده حتی اگر سیگنال را از دست داده باشد، از روی مقدار متغیر می‌فهمد که شرط قبلاً برقرار شده و بی‌جهت نمی‌خوابد.

مشکل سرقت شرط (Condition Stealing)

این مشکل معمولاً وقتی رخ می‌دهد که به‌جای while از if استفاده شود. یک رستوران با یک آشپز و دو گارسون را در نظر بگیرید. قانون رستوران این است که گارسون‌ها باید منتظر بمانند تا پیتزا آماده شود؛ تا وقتی پیتزایی نیست نباید کار را ترک کنند.

import threading
import time

kitchen_condition = threading.Condition()
pizzas_available = 0

def buggy_waiter(name):
    global pizzas_available
    print(f"[{name}] Waiting for pizza...")

    with kitchen_condition:
        if pizzas_available == 0:                # BUG: using if instead of while
            print(f"[{name}] Plate is empty. Going to wait...")
            kitchen_condition.wait()

        print(f"[{name}] I woke up! Let's take the pizza.")

        if pizzas_available > 0:
            pizzas_available -= 1
            print(f"[{name}] Yummy! I ate the pizza.")
        else:
            print(f"[{name}] ERROR: Where is the pizza? Plate is empty! (Condition Stolen)")

def chef():
    global pizzas_available
    time.sleep(1)  # cooking time
    with kitchen_condition:
        pizzas_available += 1
        print("[Chef] Made exactly 1 pizza! Waking up ALL waiters...")
        kitchen_condition.notify_all()   # wake up all waiters

w1 = threading.Thread(target=buggy_waiter, args=("Waiter 1",))
w2 = threading.Thread(target=buggy_waiter, args=("Waiter 2",))
c = threading.Thread(target=chef)

w1.start()
w2.start()
c.start()

مشکل چطور رخ می‌دهد؟ هر دو گارسون چون پیتزایی نبوده، خوابیده‌اند. آشپز یک پیتزا می‌پزد و با notify_all هر دو را بیدار می‌کند. هر دو برای گرفتن قفل هجوم می‌برند؛ گارسون اول قفل را می‌گیرد، شرط if pizzas_available > 0 را درست می‌بیند، پیتزا را برمی‌دارد و تعداد را به صفر می‌رساند. حالا گارسون دوم قفل را می‌گیرد و چون با if نوشته بودیم، اجرایش را از بعد از wait ادامه می‌دهد؛ اما تعداد پیتزا صفر شده، پس وارد else می‌شود و بدون بردن پیتزا کار را ترک می‌کند. گارسون دوم قانون را نقض کرد: او باید منتظر پیتزای بعدی می‌ماند، نه اینکه کار را رها کند.

راه‌حل، جایگزینی if با while است تا گارسونِ بیدارشده دوباره شرط را بررسی کند و اگر پیتزایی نبود، برگردد و منتظر بماند:

import threading
import time

kitchen_condition = threading.Condition()
pizzas_available = 0

def correct_waiter(name):
    global pizzas_available
    print(f"[{name}] Waiting for pizza...")

    with kitchen_condition:
        # Correct approach: use while
        while pizzas_available == 0:
            print(f"[{name}] Plate is empty. Going to wait...")
            kitchen_condition.wait()

        pizzas_available -= 1
        # When the thread wakes up, it continues from the line after wait()
        print(f"[{name}] I woke up! Let's take the pizza.")

def chef():
    global pizzas_available
    time.sleep(1)  # cooking time
    with kitchen_condition:
        pizzas_available += 1
        print("[Chef] Made exactly 1 pizza! Waking up ALL waiters...")
        kitchen_condition.notify_all()   # wake up all waiters

w1 = threading.Thread(target=correct_waiter, args=("Waiter 1",))
w2 = threading.Thread(target=correct_waiter, args=("Waiter 2",))
c = threading.Thread(target=chef)

w1.start()
w2.start()
c.start()

حالا هر گارسونی که بیدار می‌شود، دوباره شرط را چک می‌کند؛ اگر واقعاً پیتزایی موجود بود آن را برمی‌دارد، وگرنه دوباره می‌خوابد. این دقیقاً همان دلیلی است که چرا wait باید همیشه داخل while باشد.

مثال‌های واقعی از دنیای برنامه‌نویسی

مثال رستوران برای فهم مفهوم عالی است، اما ببینیم Condition در کد واقعی کجا به کار می‌آید. رایج‌ترین کاربردش الگوی «تولیدکننده/مصرف‌کننده» (Producer/Consumer) است.

مثال واقعی ۱: صف کارها (Task Queue)

تصور کنید یک «توزیع‌کننده» کارها را وارد یک صف می‌کند و یک «کارگر» منتظر می‌ماند تا هر کاری که رسید، آن را پردازش کند. کارگر نباید مدام صف را چک کند؛ فقط وقتی کاری اضافه شد بیدار می‌شود. این دقیقاً کاری است که سرورها برای پردازش درخواست‌ها انجام می‌دهند.

import threading
import time

condition = threading.Condition()
job_queue = []

def worker(worker_id):
    while True:
        with condition:
            while not job_queue:            # nothing to do yet
                condition.wait()            # sleep until a job arrives
            job = job_queue.pop(0)
        if job is None:                     # shutdown signal
            print(f"worker {worker_id} stopping")
            break
        print(f"worker {worker_id} processing {job}")
        time.sleep(0.2)

def dispatcher(jobs):
    for j in jobs:
        with condition:
            job_queue.append(j)
            condition.notify()              # wake one worker
        time.sleep(0.1)
    with condition:
        job_queue.append(None)              # tell the worker to stop
        condition.notify()

w = threading.Thread(target=worker, args=(1,))
d = threading.Thread(target=dispatcher, args=(['job-a', 'job-b', 'job-c'],))

w.start()
d.start()
w.join()
d.join()

کارگر تا وقتی صف خالی است می‌خوابد و اصلاً CPU مصرف نمی‌کند. هر بار توزیع‌کننده کاری اضافه می‌کند و notify می‌زند، کارگر بیدار می‌شود و آن را برمی‌دارد. برای پایان‌دادن هم یک مقدار ویژه (None) به صف می‌فرستیم تا کارگر بداند باید متوقف شود.

مثال واقعی ۲: بافر محدود (Bounded Buffer)

گاهی نه‌تنها مصرف‌کننده باید منتظر «پرشدن» بماند، بلکه تولیدکننده هم باید منتظر «خالی‌شدن جا» بماند؛ چون ظرفیت بافر محدود است. این حالت را «فشار برگشتی» (backpressure) می‌گویند و در کارهایی مثل پردازش ویدئو یا انتقال داده بین دو مرحله‌ی کند و تند بسیار رایج است. اینجا از notify در هر دو طرف استفاده می‌کنیم: تولیدکننده بعد از تولید، مصرف‌کننده را بیدار می‌کند و برعکس.

import threading
import time

BUFFER_MAX = 3
buffer = []
condition = threading.Condition()

def producer():
    for i in range(6):
        with condition:
            while len(buffer) == BUFFER_MAX:      # no free space
                print("buffer full, producer waits")
                condition.wait()
            buffer.append(i)
            print(f"produced {i}, buffer={buffer}")
            condition.notify()                    # wake a waiting consumer
        time.sleep(0.05)

def consumer():
    count = 0
    while count < 6:
        with condition:
            while not buffer:                     # nothing to consume
                condition.wait()
            item = buffer.pop(0)
            print(f"consumed {item}, buffer={buffer}")
            condition.notify()                    # wake a waiting producer
        count += 1
        time.sleep(0.2)

p = threading.Thread(target=producer)
c = threading.Thread(target=consumer)
p.start()
c.start()
p.join()
c.join()

وقتی مصرف‌کننده کندتر از تولیدکننده باشد، بافر پر می‌شود و تولیدکننده پشت wait منتظر می‌ماند تا جا باز شود؛ به این ترتیب حافظه بی‌کنترل رشد نمی‌کند. این همان چیزی است که کلاس آماده‌ی queue.Queue در پایتون در پشت‌صحنه با Condition پیاده کرده است — در پروژه‌های واقعی معمولاً به‌جای پیاده‌سازی دستی، مستقیماً از queue.Queue استفاده می‌کنند.

مشکل خطای زمان اجرا (RuntimeError)

متدهای wait، notify و notify_all باید حتماً وقتی فراخوانی شوند که قفل در دست ماست؛ در غیر این‌صورت با RuntimeError روبه‌رو می‌شویم. پس این متدها فقط باید در یکی از این دو محل صدا زده شوند: داخل بلاک with condition:، یا بین condition.acquire() و condition.release().

مشکل بن‌بست قفل‌های تودرتو (Nested Locks Deadlock)

این مشکل بیشتر وقتی رخ می‌دهد که Condition را در کنار قفل دیگری (مثل Lock) و به‌صورت تودرتو استفاده کنیم. یک انبار را در نظر بگیرید که برای ورود به آن به کلید اصلی (main_lock) نیاز است و برای برداشتن جنس باید منتظر آماده‌شدن آن بمانیم (item_condition).

import threading
import time

main_lock = threading.Lock()
item_condition = threading.Condition()
item_ready = False

def consumer():
    print("Consumer: Waiting to acquire the main warehouse lock...")
    with main_lock:               # Acquire the outer lock (first)
        print("Consumer: Acquired the main warehouse lock. Now waiting for the item.")
        with item_condition:      # Acquire the inner lock (second)
            while not item_ready:
                print("Consumer: Calling wait()...")
                item_condition.wait()   # <--- The deadlock happens here!
            print("Consumer: Item picked up!")

def producer():
    time.sleep(1)                 # Wait for the consumer to acquire the lock first
    global item_ready
    print("Producer: I want to put the item in the warehouse. Waiting for the main lock...")

    with main_lock:               # Producer gets stuck here!
        with item_condition:
            item_ready = True
            item_condition.notify()
            print("Producer: Item is ready and notification sent.")

t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=producer)

t1.start()
t2.start()

چرا بن‌بست می‌شود؟ مصرف‌کننده ابتدا main_lock و بعد item_condition را می‌گیرد و به wait می‌رسد. متد wait فقط قفل item_condition را آزاد می‌کند، نه main_lock را! پس main_lock همچنان در دست مصرف‌کننده می‌ماند. حالا تولیدکننده برای رساندن جنس به main_lock نیاز دارد اما آن را در دست مصرف‌کننده می‌بیند و گیر می‌کند؛ در نتیجه هرگز notify نمی‌کند و مصرف‌کننده هم تا ابد در wait می‌ماند. درس این مثال: از قفل‌گذاری تودرتو پرهیز کنید و ترتیب گرفتن قفل‌ها را به‌گونه‌ای طراحی کنید که چنین حلقه‌ی انتظاری شکل نگیرد.

مشکل افت شدید عملکرد (Performance Bottleneck)

این مشکل وقتی پیش می‌آید که کارهای زمان‌بری را که اصلاً به منبع مشترک ربطی ندارند، داخل قفل انجام دهیم. به کد زیر توجه کنید:

def chef():
    global pizzas_available
    with kitchen_condition:
        time.sleep(1)  # cooking time
        pizzas_available += 1
        print("[Chef] Made exactly 1 pizza! Waking up ALL waiters...")
        kitchen_condition.notify_all()   # wake up all waiters

مشکل، قرارگرفتن sleep (پخت پیتزا) داخل بلاک with است. پخت پیتزا هیچ تداخلی بین تردها ایجاد نمی‌کند و نیازی نیست داخل قفل باشد. با گذاشتن آن داخل قفل، گارسون‌ها بی‌جهت یک ثانیه‌ی اضافه منتظر می‌مانند تا آشپز قفل را آزاد کند. قاعده: فقط بخشی که واقعاً به منبع مشترک دست می‌زند را داخل قفل نگه دارید و کارهای سنگین و مستقل را بیرون از قفل انجام دهید.

خلاصه

  • Condition برای هماهنگی «منتظرماندن تا وقوع یک شرط» بین تردها (الگوی تولیدکننده/مصرف‌کننده) استفاده می‌شود و از busy-waiting جلوگیری می‌کند.
  • متدهای wait، notify و notify_all را همیشه داخل قفل (with condition) صدا بزنید، وگرنه RuntimeError می‌گیرید.
  • همیشه wait را داخل یک حلقه‌ی while که یک شرط واقعی را بررسی می‌کند بگذارید تا از بیدارشدن کاذب و سرقت شرط در امان بمانید.
  • مراقب ترتیب wait و notify (ازدست‌رفتن سیگنال)، قفل‌های تودرتو (بن‌بست) و انجام کارهای سنگین داخل قفل (افت عملکرد) باشید.
  • در پروژه‌های واقعی برای صف تولیدکننده/مصرف‌کننده معمولاً به‌جای پیاده‌سازی دستی از queue.Queue استفاده می‌شود که در پشت‌صحنه بر پایه‌ی Condition ساخته شده است.
Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (21%)
نظرات

دیدگاهتان را بنویسید

21%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم