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

Please login to bookmark Close

وقتی چند ترد (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 نشوید. با همین چند متد ساده می‌توانید هماهنگی‌های نسبتاً پیچیده‌ای بین تردها بسازید.

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

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

19%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

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

پاسخ تمرین ها

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

اشتراک گذاری

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

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

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

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

تنظیمات

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