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

Please login to bookmark Close

چند کروتین دارید که همه آمادهٔ کارند، اما هیچ‌کدام نباید یک لحظه زودتر از «چراغ سبز» شروع کند؛ مثلاً تا وقتی تنظیمات اولیه بار نشده یا اتصال به دیتابیس برقرار نشده، هیچ کارگری نباید درخواستی را پردازش کند. اولین وسوسه این است که یک پرچم ساده بگذارید و مدام چکش کنید. بیایید ببینیم چرا این کار اشتباه است و چطور کلاس Event در asyncio دقیقاً همین مسئله را تمیز حل می‌کند.

مشکل: انتظار با حلقهٔ چک کردن

به این نسخهٔ ساده و ناقص نگاه کنید. کارگر تا آماده شدن سیستم منتظر می‌ماند، اما با چک کردن مکرر یک متغیر:

import asyncio

ready = False   # a plain flag shared between coroutines

async def worker(name):
    while not ready:            # busy-wait: wakes up again and again for nothing
        await asyncio.sleep(0.01)
    print(f'{name}: started')

async def setup():
    await asyncio.sleep(2)      # loading config / warming up
    global ready
    ready = True

async def main():
    await asyncio.gather(worker('A'), worker('B'), setup())

asyncio.run(main())

چرا این بد است؟ کارگر هر ۱۰ میلی‌ثانیه بی‌دلیل بیدار می‌شود، حلقه را می‌چرخاند و دوباره می‌خوابد. این هم CPU را هدر می‌دهد و هم واکنش را کند می‌کند؛ اگر بازهٔ خواب را کم کنید حریص‌تر می‌شود و اگر زیاد کنید دیرتر پاسخ می‌دهد. آنچه واقعاً می‌خواهیم این است: کارگر بخوابد و فقط در لحظهٔ رسیدن سیگنال بیدار شود، نه یک لحظه زودتر و نه با هدر دادن چرخه.

راه‌حل با کلاس Event در asyncio

یک asyncio.Event() بسازید. این شیء در ابتدا «تنظیم‌نشده» است و is_set() مقدار False برمی‌گرداند. هر کروتینی که await event.wait() را صدا بزند همان‌جا معلق می‌ماند تا کسی set() را فراخوانی کند؛ آن‌گاه همهٔ منتظرها با هم آزاد می‌شوند:

import asyncio

async def worker(event, name):
    print(f'{name}: waiting for green light...')
    await event.wait()          # suspends here with zero CPU until the event is set
    print(f'{name}: started')

async def setup(event):
    await asyncio.sleep(2)       # loading config / warming up
    print('setup: ready, sending signal')
    event.set()                 # wakes every coroutine parked on wait() at once

async def main():
    event = asyncio.Event()
    await asyncio.gather(
        worker(event, 'A'),
        worker(event, 'B'),
        setup(event),
    )

asyncio.run(main())

حالا کارگرها تا لحظهٔ set() هیچ چرخه‌ای مصرف نمی‌کنند و دقیقاً هم‌زمان بیدار می‌شوند. توجه کنید که Event داده منتقل نمی‌کند؛ فقط یک سیگنالِ «رخ داد / رخ نداد» است. اگر نیاز به عبور دادن مقدار دارید، asyncio.Queue ابزار درست است، نه Event.

یک مثال واقعی: دروازهٔ آماده‌به‌کار سرویس

فرض کنید یک سرویس وب دارید که چند هندلر هم‌زمان بالا می‌آیند، اما هیچ‌کدام نباید تا گرم شدن کش پاسخ بدهند. یک Event نقش «دروازه» را بازی می‌کند:

import asyncio

async def handler(cache_ready, req):
    await cache_ready.wait()     # every request parks here until cache is warm
    print(f'request {req}: served')

async def warm_cache(cache_ready):
    await asyncio.sleep(1.5)     # fill the cache from a slow source
    cache_ready.set()

async def main():
    cache_ready = asyncio.Event()
    await asyncio.gather(
        warm_cache(cache_ready),
        *[handler(cache_ready, i) for i in range(5)],
    )

asyncio.run(main())

نکتهٔ ظریف: اگر یک درخواست بعد از set() برسد، wait() فوراً برمی‌گردد چون Event هنوز در حالت تنظیم‌شده است. یعنی نیازی نیست نگران «از دست رفتن سیگنال» برای تازه‌واردها باشید.

پیتفال: چرخهٔ set() و بلافاصله clear()

مهم‌ترین دامی که باید بشناسید این است: اگر برای «یک‌بار مصرف» کردن سیگنال، بلافاصله بعد از set() متد clear() را صدا بزنید، ممکن است کروتین‌هایی که هنوز نوبت اجرایشان نرسیده اصلاً سیگنال را نبینند. دلیلش این است که wait() تنها زمانی از خواب بیدار می‌شود که حلقهٔ رویداد کنترل را به آن بازگرداند؛ اگر پیش از آن Event را پاک کنید، آن‌ها یک Event تنظیم‌نشده می‌بینند و برای همیشه معطل می‌مانند. به‌یاد داشته باشید: Event برای الگوی «یک‌بار روشن، همیشه روشن» عالی است؛ برای سیگنال‌های تکرارشونده و شرط‌دار بهتر است سراغ Condition بروید، درست مثل نسخهٔ threading آن.

جمع‌بندی

  • کلاس Event در asyncio یک سیگنال ساده بین کروتین‌هاست؛ به‌جای حلقهٔ چک کردن، کروتین را بدون مصرف CPU معلق نگه می‌دارد.
  • set() همهٔ منتظرهای wait() را هم‌زمان آزاد می‌کند؛ اگر Event از قبل تنظیم شده باشد، wait() فوراً برمی‌گردد.
  • Event داده منتقل نمی‌کند؛ برای انتقال مقدار از Queue و برای شرط‌های پیچیده از Condition استفاده کنید.
  • از set() و بلافاصله clear() پرهیز کنید؛ خطر از دست رفتن سیگنال برای کروتین‌هایی که هنوز اجرا نشده‌اند وجود دارد.
Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (60%)
نظرات

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

60%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

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

پاسخ تمرین ها

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

اشتراک گذاری

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

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

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

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

تنظیمات

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