درک عملکرد کلاس 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 باعث میشود که گارسون ها همیشه یک ثانیه بیشتر منتظر بمانند تا آشپز قفل را آزاد کند.