
وقتی چند ترد (Thread) بهصورت همزمان اجرا میشوند، دیر یا زود به این نیاز میرسیم که یک ترد بتواند به تردهای دیگر «خبر بدهد» که اتفاقی افتاده است؛ مثلاً «دادهها آماده شد»، «حالا میتوانید شروع کنید» یا «وقت توقف است». کلاس threading.Event سادهترین ابزار پایتون برای همین «خبر دادن» یا سیگنالدهی بین تردهاست. در این مقاله ابتدا مفهوم آن را با یک مثال ملموس یاد میگیریم، بعد مکانیزم درونی و همهی متدهایش را بررسی میکنیم و در نهایت با چند مثال کاربردی و نکات مهم، تصویر کاملی از آن به دست میآوریم.
مفهوم Event
فرض کنید چند دونده روی خط شروع ایستادهاند. آنها کاملاً آمادهی شروع هستند، اما حق ندارند حرکت کنند تا زمانی که داور تپانچه را شلیک کند.
در پایتون، threading.Event دقیقاً نقش تپانچهی داور را برای دوندهها (تردها) بازی میکند. این کلاس یک راه بسیار ساده است تا یک ترد بتواند به تردهای دیگر بگوید: «صبر کنید… صبر کنید… حالا حرکت کنید!»
برای درک بهتر، دو نوع مسابقهی دو را در نظر بگیرید:
- مسابقهی دوی صد متر: داور تپانچه را یک بار شلیک میکند (Event سیگنال میدهد) و دوندهها (تردها) شروع میکنند تا به خط پایان برسند. فقط یک سیگنال لازم است.
- مسابقهی دوی چند مرحلهای: داور تپانچه را شلیک میکند و دوندهها شروع به حرکت میکنند تا به خط پایان مرحلهی اول برسند. همه باید آنجا منتظر بمانند تا داور دوباره تپانچه را برای شروع مرحلهی بعد شلیک کند. اینجا به چند سیگنال پشتسرهم نیاز داریم.
مسابقهی دوی صد متر (یک سیگنال)
در این مسابقه فقط یک خط پایان وجود دارد. همهی تردها صبر میکنند تا اجازهی شروع صادر شود و بعد یکجا حرکت میکنند تا به خط پایان برسند. این سادهترین کاربرد Event است.
مسابقهی دوی چند مرحلهای (چند سیگنال)
در این مسابقه چندین خط پایان وجود دارد؛ یعنی تردها باید چند بار صبر کنند و منتظر بمانند تا اجازهی شروع مرحلهی بعد صادر شود. در این سناریو یک چالش مهم ممکن است پیش بیاید: تصور کنید یکی از دوندهها آنقدر دیر به خط پایان مرحلهی اول برسد که داور پیش از رسیدن او تپانچه را برای مرحلهی بعد شلیک کرده و بقیه آن مرحله را پشت سر گذاشته باشند. در این حالت آن دوندهی دیرآمده فکر میکند که هنوز نوبت مرحلهی قبل است و بیجهت منتظر میماند، در حالی که بقیه خیلی وقت است جلو رفتهاند. نتیجه، یک هماهنگی بههمریخته و بروز خطاست. کمی جلوتر میبینیم که چطور با clear() از این مشکل جلوگیری کنیم.
حالا که عملکرد Event را در قالب مفهوم فهمیدیم، ببینیم این ایدهها در سطح کد چطور پیادهسازی میشوند.
مکانیزم درونی Event
- کلاس
Eventدر دل خود فقط یک متغیر ساده از نوعBoolean(یک پرچم یا Flag) دارد که مقدار اولیهاشFalseاست. - تا وقتی این پرچم
Falseاست، هر تردی که روی اینEventمنتظر بماند، متوقف میشود و پیش نمیرود. - بهمحض اینکه پرچم
Trueشود، تمام تردهای منتظر همزمان آزاد میشوند و کارشان را ادامه میدهند. - با متد
wait()یک ترد را در حالت انتظار نگه میداریم. - با متد
set()پرچم راTrueمیکنیم و همهی تردهای منتظر را آزاد میکنیم. - با متد
clear()پرچم را دوبارهFalseمیکنیم تا تردها بار دیگر بتوانند در حالت انتظار قرار بگیرند.
نکتهی کلیدی این است که Event یک سیگنال «چسبنده» است: وقتی set() صدا زده شود، پرچم True باقی میماند و هر تردی که بعداً wait() کند، بدون معطلی رد میشود؛ مگر اینکه با clear() دوباره پرچم را پایین بیاوریم.
متدهای کلاس Event در یک نگاه
| متد | کار آن |
|---|---|
set() | پرچم داخلی را True میکند. همهی تردهای منتظر بلافاصله آزاد میشوند. |
clear() | پرچم را دوباره False میکند. تردهایی که بعد از این wait() کنند، تا set() بعدی متوقف میمانند. |
wait(timeout=None) | ترد را متوقف میکند تا پرچم True شود. اگر پرچم از قبل True باشد، بیدرنگ رد میشود. با دادن timeout حداکثر همان تعداد ثانیه صبر میکند و مقدار True (سیگنال رسید) یا False (زمان تمام شد) برمیگرداند. |
is_set() | اگر پرچم فعلاً True باشد True برمیگرداند، در غیر اینصورت False. برای بررسی وضعیت بدون متوقفشدن مفید است. |
مثال ۱ – شبیهسازی مسابقهی صد متر
در تکهکد زیر یک مسابقهی دو شبیهسازی شده است. سه دونده باید صبر کنند تا داور تپانچه را شلیک کند و بعد با هم شروع کنند.
import threading
import time
def worker(runner_id, start_signal):
print(f"Runner {runner_id} is ready")
start_signal.wait() # Stops here until the signal arrives
print(f"Runner {runner_id} is running")
# 1. Create the Event (Initial state is False / Red Light)
start_signal = threading.Event()
# 2. Create and start 3 runner threads
for i in range(1, 4):
t = threading.Thread(target=worker, args=(i, start_signal))
t.start()
print("Please Wait!")
time.sleep(3)
print("GO!")
# 3. Fire the gun! All three runners are released at exactly the same moment.
start_signal.set()در تکهکد بالا:
- تابع
workerیک دونده را شبیهسازی میکند که ابتدا اعلام آمادگی میکند و سپس رویstart_signal.wait()متوقف میشود تا سیگنال شروع برسد. - با
start_signal = threading.Event()همان «تپانچهی داور» ساخته میشود (در ابتدا خاموش /False). - حلقهی
forسه ترد را میسازد وstart()میکند؛ هر سه بلافاصله به حالت انتظار میروند. - یک وقفهی سه ثانیهای با
time.sleep(3)ایجاد میشود تا مطمئن شویم همه آمادهی انتظارند. - فراخوانی
start_signal.set()پرچم راTrueمیکند؛ یعنی تپانچه شلیک میشود. در همین لحظه هر سهwait()آزاد میشوند و پیام «running» چاپ میشود.
مثال ۲ – انتظار با محدودیت زمان (timeout)
همیشه نمیخواهیم یک ترد تا ابد منتظر بماند. متد wait() یک پارامتر timeout میگیرد؛ اگر سیگنال در آن بازه برسد مقدار True و اگر نرسد مقدار False برمیگرداند. این ویژگی برای جلوگیری از انتظار بیپایان بسیار مفید است.
import threading
import time
event = threading.Event()
def worker():
print("Waiting for signal (max 3 seconds)...")
received = event.wait(timeout=3) # Returns True or False
if received:
print("Signal received!")
else:
print("Timeout; no signal received.")
t = threading.Thread(target=worker)
t.start()
# Since we never call set(), after 3 seconds the "timeout" message will be printed.مثال ۳ – بررسی وضعیت با is_set()
گاهی فقط میخواهیم بدانیم پرچم روشن است یا خاموش، بدون اینکه ترد را متوقف کنیم. برای این کار از is_set() استفاده میکنیم.
import threading
event = threading.Event()
print(event.is_set()) # False (initial state)
event.set()
print(event.is_set()) # True
event.clear()
print(event.is_set()) # Falseمثال ۴ – مسابقهی چند مرحلهای و نقش clear()
حالا تصور کنید مسابقه چند مرحله دارد. شروع هر مرحله با شلیک تپانچه است و دوندهها پس از پایان هر مرحله باید صبر کنند تا داور اجازهی مرحلهی بعد را بدهد. پس تردها بعد از اتمام کار هر مرحله باید دوباره به حالت انتظار بروند. برای اینکه یک ترد را دوباره منتظر کنیم، باید پیش از wait() دوم از clear() استفاده کنیم تا پرچم که هنوز از سیگنال قبلی True مانده، خاموش شود.
import threading
import time
event = threading.Event()
def worker():
print("Waiting for first signal...")
event.wait()
print("First signal received! Working for 2 seconds...")
time.sleep(2)
event.clear() # Turn off the flag so it's ready for the next signal
print("Waiting for second signal...")
event.wait()
print("Second signal received! Work done!")
t = threading.Thread(target=worker)
t.start()
time.sleep(1)
event.set() # First signal
time.sleep(3)
event.set() # Second signalاگر یک ترد باید چند بار از wait() استفاده کند، از بار دوم به بعد باید پیش از هر wait() یک clear() بزند تا مطمئن شود واقعاً وارد حالت انتظار میشود. اگر این کار را نکند، ممکن است پرچم هنوز True باشد و ترد بدون هیچ توقفی از روی wait() رد شود.
روشهای اجرای مرحلهبهمرحله
برای اجرای مرحلهبهمرحله، تردها باید بین هر مرحله منتظر بمانند تا اجازهی ورود به مرحلهی بعد را پیدا کنند. این کار به دو روش امکانپذیر است.
۱. فراخوانی clear() قبل از wait() توسط خود ترد
این همان روشی است که در مثال قبلی دیدیم. خود ترد بعد از دریافت هر سیگنال، Event را clear() میکند تا برای سیگنال بعدی آماده شود.
def worker():
event.wait() # Waiting for the first signal
print("Signal 1 received")
event.clear() # Turn off the Event for the next signal
event.wait() # Waiting for the second signal
print("Signal 2 received")
event.clear() # Turn off the Event for the next signal
event.wait() # Waiting for the third signal
print("Signal 3 received")۲. ایجاد Event جدید برای هر مرحله
بهجای استفادهی مجدد از یک Event، میتوانید برای هر مرحله یک Event جداگانه بسازید. این کار نیاز به clear() را کاملاً از بین میبرد.
import threading
import time
event1 = threading.Event()
event2 = threading.Event()
event3 = threading.Event()
def worker():
event1.wait()
print("Signal 1 received")
event2.wait()
print("Signal 2 received")
event3.wait()
print("Signal 3 received")
t = threading.Thread(target=worker)
t.start()
time.sleep(1)
event1.set()
time.sleep(1)
event2.set()
time.sleep(1)
event3.set()مزیت: کد سادهتر است و نیازی به clear() ندارید.
عیب: اگر تعداد مراحل زیاد باشد، مدیریت چندین Event سخت و شلوغ میشود.
بهترین روش در یک نگاه
اگر تعداد مراحل مشخص و کم است (مثلاً ۲ تا ۳ مرحله)، ساختن Event جدید برای هر مرحله سادهترین راه است. اما اگر تعداد مراحل زیاد است یا نمیخواهید چندین Event را مدیریت کنید، روش clear() داخل خود ترد گزینهی بهتری است.
مثال ۵ – توقف امنِ یک ترد پسزمینه
یکی از پرکاربردترین استفادههای واقعی Event، سیگنالِ «توقف» برای تردهای پسزمینه است. بهجای time.sleep() از event.wait(timeout=...) استفاده میکنیم؛ به این ترتیب ترد در فاصلههای زمانی کار میکند اما هر لحظه که سیگنال توقف برسد، بلافاصله بیدار میشود و خارج میشود؛ نه اینکه تا پایان sleep منتظر بماند.
import threading
import time
stop_event = threading.Event()
def background_task():
while not stop_event.is_set():
print("Running background task...")
# Use wait instead of time.sleep so it can be stopped immediately
stop_event.wait(timeout=1)
print("Background task stopped.")
t = threading.Thread(target=background_task)
t.start()
time.sleep(2.5)
print("Requesting stop...")
stop_event.set() # Stop signal
t.join() # Wait for the thread to cleanly terminateاین الگو («flag توقف» بهعلاوهی wait بهجای sleep) در حلقههای نظارتی، کارهای زمانبندیشده و سرویسهای دائمدرحالاجرا بسیار رایج است، چون امکان خاموشکردن تمیز و سریع برنامه را فراهم میکند.
نکتهی بسیار مهم در استفاده از clear()
هنگام استفاده از clear() داخل یک ترد، باید به این نکتهی حیاتی توجه داشته باشید:
clear() فقط وضعیت Event را برای تردهایی که بعداً روی آن wait() میکنند تغییر میدهد. تردهایی که همزمان با تردِ clear()کننده آزاد شدهاند، تحت تأثیر قرار نمیگیرند.
خطر اصلی زمانی رخ میدهد که چند ترد از یک Event مشترک استفاده میکنند. برای مثال فرض کنید دو ترد (worker1 و worker2) روی یک Event منتظرند. ترد اصلی set() را صدا میزند و هر دو آزاد میشوند. حالا اگر worker1 سریعتر به خط clear() برسد و Event را خاموش کند، worker2 که هنوز مشغول کار است متوجه این تغییر نمیشود. مشکل از جایی شروع میشود که همین worker2 (یا ترد دیگری) بعداً دوباره روی همان Event که اکنون خاموش است wait() کند؛ در این حالت آن ترد برای همیشه منتظر میماند و برنامه دچار بنبست (Deadlock) میشود.
بنابراین استفاده از روش clear() درون ترد تنها زمانی امن است که مطمئن باشید فقط یک ترد از آن Event استفاده میکند و هیچ ترد دیگری قرار نیست دوباره منتظر همان Event شود. در سناریوهای چندتردی با مراحل متعدد، ساختن Eventهای جداگانه برای هر مرحله رویکرد امنتری است.
جمعبندی
کلاس threading.Event سادهترین و در عین حال یکی از پرکاربردترین ابزارهای هماهنگی بین تردهاست. کل ماجرا حول یک پرچم Boolean میچرخد: با set() آن را روشن و همهی منتظرها را آزاد میکنید، با clear() دوباره خاموش میکنید، با wait() منتظر میمانید (در صورت نیاز با timeout) و با is_set() وضعیت را بدون توقف میخوانید. برای سیگنالِ یکباره کافی است یک set() بزنید؛ برای سناریوهای چند مرحلهای یا از clear() کنترلشده استفاده کنید یا برای هر مرحله یک Event جدا بسازید؛ و همیشه حواستان به تلهی clear() در محیطهای چندتردی باشد تا گرفتار Deadlock نشوید. با همین چند متد ساده میتوانید هماهنگیهای نسبتاً پیچیدهای بین تردها بسازید.