برای استفاده از همه امکانات سایت از فیلتر شکن استفاده کنید.
برای استفاده از همه امکانات سایت از فیلتر شکن استفاده کنید.

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

Please login to bookmark Close

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

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

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

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

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

acquire و release

برای بستن و باز کردن قفل به کار می‌روند. همچنین به جای این متد ها از دستور with هم می‌توان استفاده کرد.

wait

ترد را منتظر می‌گذارد تا کسی صدایش کند.

notify

یک تردِ منتظر را صدا می‌کند.

notify_all

اگر چند ترد منتظر باشند، همه را با هم صدا می‌کند.

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

به تکه کد زیر توجه کنید. این همان رستورانی است که ابتدای این بخش در موردش صحبت کردیم.

یک آشپز (chef) داریم که برای آماده سازی سفارش یک ثانیه وقت لازم دارد. یک گارسون (waiter) هم داریم که همیشه منتظر می‌ماند تا به محض آماده شدن آن را به مشتری تحویل دهد.

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

در تکه کد بالا:

  • تابع waiter با استفاده از متد wait منتظر می‌ماند تا سفارش آماده شود.
  • تابع chef پس از یک ثانیه با فراخوانی notify به گارسون اطلاع می‌دهد که سفارش آماده است.

بررسی اجرای مرحله به مرحله کد بالا:

  • خط ۵: یک قفل ساخته می‌شود.
  • خط ۲۴ تا ۲۹: ترد گارسون و آشپز ساخته و اجرا می‌شوند.
  • خط ۱۶: ترد آشپز مشغول پخت پیتزا می‌شود.
  • خط۱۰: در زمانی که ترد آشپز در حال پخت است، گارسون به قفل می‌رسد و آن را در این قسمت می‌بندد.
  • خط ۱۱: گارسون wait را می‌بیند و در همین قسمت به خواب فرو رفته و قفل را آزاد می‌کند و صبر می‌کند تا آشپز با فراخوانی notify آن را بیدار کند.
  • خط ۱۸: با توجه به اینکه گارسون قفل را آزاد کرد، اکنون آشپز می‌تواند پس از پخت پیتزا، اینجا قفل را ببندد.
  • خط ۲۱: با فراخوانی notify گارسون بیدار شده و اجرای کد هایش از همانجایی که به خواب رفته بوده (خط ۱۱) ادامه می‌یابد.

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

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

برای اینکه یک ترد را دوباره بخوابانیم باید متد wait را دوباره صدا بزنیم. این کار را با قرار دادن متد 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 ساخته می‌شود.
  • خط ۱۴ تا ۱۷: قفل بسته می‌شود و چون مقدار is_pizza_ready هنوز False است متد wait اجرا شده و گارسون قفل را آزاد می‌کند.
  • خط ۲۶ تا ۳۰: حالا که گارسون قفل را باز کرده، در این قسمت آشپز قفل را می‌بندد و مقدار is_pizza_ready را برابر True تنظیم می‌کند تا تائیدی بر این باشد که ما خودمان notify را صدا می‌کنیم.
  • خط ۱۶ و ۱۷: حالا که بلاک with از خط ۲۶ تا ۳۰ کامل اجرا شده، اکنون یک بار دیگر این حلقه اجرا می‌شود. این بار چون مقدار is_pizza_ready برابر True است، گارسون می‌فهمد که واقعا آشپز بوده که گارسون را بیدار کرده نه سیستم عامل به صورت اتفاقی!

مشکل Race Condition در اثر از دست رفتن سیگنال (Missed Notification / Signal)

همیشه باید اول wait و سپس notify فراخوانی شود.

اگر اول notify فراخوانی شود، کسی منتظر دریافت سیگنال نخواهد بود و آن سیگنال گم می‌شود. در این شرایط wait منتظر دریافت سیگنال می‌ماند اما از آنجایی که قبلا notify سیگنال را ارسال کرده است، تا ابد منتظر دریافت سیگنال می‌ماند.

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

import threading
import time

kitchen_condition = threading.Condition()

def waiter():
    # گارسون قبل از رفتن به آشپزخانه، مشغول کار دیگری می‌شود
    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() # گارسون اینجا تا ابد می‌خوابد!
        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() # آشپز زنگ را می‌زند و می‌رود

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

waiter_thread.start()
chef_thread.start()

در تکه کد بالا، (عمدا کاری کرده ایم که!) notify قبل از wait سیگنال را ارسال می‌کند. این باعث می‌شود که wait تا ابد منتظر دریافت سیگنالی بماند که قبلا ارسال شده است.

مشکل Race Condition در اثر سرقت منابع (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:
            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) # زمان پخت
    with kitchen_condition:
        pizzas_available += 1
        print("[Chef] Made exactly $1$ pizza! Waking up ALL waiters...")
        kitchen_condition.notify_all() # بیدار کردن همه گارسون‌ها

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

ترتیب اجرای کد های بالا به شرح زیر است:

  • ابتدا ترد ها از خط ۳۲ تا ۳۸ ساخته و اجرا می‌شوند.
  • هر سه ترد اجرا می‌شوند. در حالی که ترد آشپز در خط ۲۶ مشغول پخت می‌شود، گارسون اول در خط ۱۱ قفل را می‌گیرد و گارسون دوم را در آنجا منتظر آزاد شدن قفل می‌گذارد.
  • گارسون اول با اجرای خط ۱۲ بررسی می‌کند که آشپز پیتزا را آماده کرده یا نه و چون پیتزا هنوز آماده نشده، در خط ۱۴ با اجرای ‍wait به خواب فرو می‌رود و قفل را برای گارسون دوم آزاد می‌کند.
  • گارسون دوم قفل را در خط ۱۱ می‌گیرد و در خط ۱۲ بررسی می‌کند که آشپز پیتزا را آماده کرده یا نه و چون پیتزا هنوز آماده نشده، در خط ۱۴ با اجرای wait به خواب فرو می‌رود و قفل را آزاد می‌کند.
  • آشپز پخت اولین پیتزا را در خط ۲۶ به پایان می‌رساند و از آنجایی که گارسون ها قبلا قفل را آزاد کرده اند، با اجرای خط ۲۷ قفل را میگیرد و سپس در خط ۲۸ اعلام می‌کند که یک پیتزا آماده شده است. سپس با اجرای notify_all این را به گارسون ها اطلاع می‌دهد.
  • اکنون هر دو گارسون در خط ۱۴ از خواب بیدار شده و هجوم می‌برند تا اولین نفری باشند که قفل را در اختیار می‌گیرد.
  • (فرض می‌کنیم) گارسون اول در این رقابت قفل را به دست می‌آورد. در این صورت با اجرای شرطی که در خط ۱۸ نوشته شده، می‌فهمد که یک پیتزا آماده شده است. او در خط ۱۹ مقدار پیتزا های آماده را یک واحد کم می‌کند و سپس آن پیتزا را برای مشتری می‌برد. در اینجا کار گارسون اول تمام می‌شود.
  • اکنون که کار گارسون اول تمام شده است، گارسون دوم فرصت می‌کند تا قفلی که آزاد شده را بگیرد. گارسون دوم به محض اینکه قفل را می‌گیرد، از خط ۱۴ اجرایش را ادامه می‌دهد تا به خط ۱۸ برسد. در این خط چون قبلا گارسون اول یک واحد از تعداد پیتزا ها کم کرده و تعداد را به صفر رسانده، گارسون دوم فکر می‌کند که دیگر پیتزایی برای آماده کردن وجود ندارد و با او کاری نداریم. به همین دلیل وارد else شده و بدون اینکه پیتزایی را به مشتری برساند کارش را تمام می‌کند.

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

برای حل مشکل، باید گارسون دوم را وادار کنیم که منتظر آماده شدن پیتزا بماند.

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:
        # ✅ روش صحیح: استفاده از while
        while pizzas_available == 0:
            print(f"[{name}] Plate is empty. Going to wait...")
            kitchen_condition.wait()
            
        pizzas_available -= 1
        # وقتی ترد بیدار می‌شود، از خط بعد از wait ادامه می‌دهد
        print(f"[{name}] I woke up! Let's take the pizza.")



def chef():
    global pizzas_available
    time.sleep(1) # زمان پخت
    with kitchen_condition:
        pizzas_available += 1
        print("[Chef] Made exactly $1$ pizza! Waking up ALL waiters...")
        kitchen_condition.notify_all() # بیدار کردن همه گارسون‌ها

# ایجاد $2$ گارسون و $1$ آشپز
w1 = threading.Thread(target=buggy_waiter, args=("Waiter 1",))
w2 = threading.Thread(target=buggy_waiter, args=("Waiter 2",))
c = threading.Thread(target=chef)
# c2 = threading.Thread(target=chef)

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

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

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

اگر متد های wait و notify و notify_all بیرون از قفل فراخوانی شوند با این خطا مواجه می‌شویم. به عبارت دیگر فقط در یکی از دو مکان زیر باید فراخوانی شوند.

  • درون بلاک with
  • بین acquire و release
  • مشکل بن‌بست به دلیل خطا (Exception before Notify)

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

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

مثال – انبار و سامانه ثبت

فرض کنید یک انبار داریم که برای ورود به آن به یک کلید اصلی (main_lock) نیاز داریم و برای برداشتن جنس باید منتظر بمانیم تا جنس آماده شود (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 disaster (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()

مراحل اجرای کد بالا به شرح زیر است:

  • ابتدا در خط ۲۹ تا ۳۳ ترد ها ساخته و اجرا می‌شوند.
  • زمانی که producer در خط ۱۹ مشغول آماده سازی است، consumer ابتدا قفل main_lock را در خط ۱۰ و سپس قفل item_condition را در خط ۱۲ می‌بندد. consumer همینطور به اجرا ادامه داده ودر خط ۱۳ که می‌بیند item_ready هنوز مقدار ندارد، باز هم ادامه می‌دهد تا در خط ۱۵ به wait می‌رسد. در اینجا به خواب فرو رفته و قفل را آزاد می‌کند. اما قفلی که آزاد می‌شود item_condition است.
  • سپس producer که مراحل آماده سازی را در خط ۱۹ کامل کرده به خط ۲۳ می‌رسد و می‌خواهد قفل را تحویل بگیرد اما نمی‌تواند زیرا main_lock هنوز در دست consumer است.

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

این مشکل زمانی رخ می‌دهد که کار های زمان بری که نیازی به منابع مشترک ندارند را داخل قفل انجام دهیم.

به تکه کد زیر توجه کنید.

def chef():
    global pizzas_available
    with kitchen_condition:
        time.sleep(1) # زمان پخت
        pizzas_available += 1
        print("[Chef] Made exactly $1$ pizza! Waking up ALL waiters...")
        kitchen_condition.notify_all() # بیدار کردن همه گارسون‌ها

مشکل کد بالا، قرار گرفتن sleep در بلاک with است. پخت پیتزا تداخلی بین ترد ها ایجاد نمی‌کند و نباید داخل بلاک with باشد.

قرار گرفتن sleep در بلاک with باعث می‌شود که گارسون ها همیشه یک ثانیه بیشتر منتظر بمانند تا آشپز قفل را آزاد کند.

Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (39%)
نظرات

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

39%
پیشرفت
فهرست مطالب

تمرین

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

پاسخ تمرین ها

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

اشتراک گذاری

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

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

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

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

تنظیمات

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