آموزش ماژول asyncio – متد shield

Please login to bookmark Close

تصور کنید کاربر وسط یک تراکنش اتصالش را قطع می‌کند یا مهلت درخواست تمام می‌شود و شما Task را لغو می‌کنید. اما درست در همین لحظه، کد در حال نوشتن نهایی در پایگاه‌داده بود. اگر این نوشتن نیمه‌کاره قطع شود، دادهٔ خراب می‌ماند. سؤال این است: چطور یک Task بیرونی را لغو کنیم ولی یک عملیات حساس درونی از این لغو در امان بماند؟ پاسخ، تابع asyncio.shield() است.

مشکل: لغوی که عملیات حساس را قطع می‌کند

به این نسخهٔ ساده نگاه کنید. یک Task پردازش داریم که درونش یک نوشتن حساس را await می‌کند. وقتی Task بیرونی را لغو کنیم، لغو تا عمق نفوذ می‌کند:

import asyncio

async def write_to_db():
    print('db: writing...')
    await asyncio.sleep(3)            # the critical write in progress
    print('db: commit done')          # never printed if cancelled midway

async def worker():
    await write_to_db()               # no protection: cancel reaches the write

async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(1)
    task.cancel()                     # user disconnected / timeout
    try:
        await task
    except asyncio.CancelledError:
        print('main: worker cancelled')
    await asyncio.sleep(3)

asyncio.run(main())

چرا این خراب است؟ خروجی خط db: commit done را هرگز نمی‌بینید؛ لغو دقیقاً وسط await asyncio.sleep(3) نوشتن را قطع کرد و نوشتن نیمه‌کاره ماند. ما می‌خواهیم worker لغو شود، اما نوشتن دیتابیس به پایان برسد.

راه‌حل با asyncio.shield()

عملیات حساس را در یک Task جداگانه بگذارید و آن را داخل asyncio.shield() بپوشانید. اگر کدی که منتظر shield است لغو شود، خطای CancelledError فقط به نقطهٔ await می‌رسد، اما خود Task داخلی در پس‌زمینه زنده می‌ماند:

import asyncio

async def write_to_db():
    print('db: writing...')
    await asyncio.sleep(3)
    print('db: commit done')          # now protected, still runs to the end

async def worker():
    db_task = asyncio.create_task(write_to_db())
    try:
        await asyncio.shield(db_task)  # cancel hits here, NOT db_task itself
    except asyncio.CancelledError:
        print('worker: cancelled, but db_task keeps running')
        raise

async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print('main: worker cancelled')
    await asyncio.sleep(3)            # give db_task time to finish

asyncio.run(main())

حالا worker لغو می‌شود، اما db_task که shield شده بود به کار ادامه می‌دهد و در نهایت db: commit done را چاپ می‌کند. دقیقاً همان چیزی که می‌خواستیم. توجه کنید که shield از انتشار لغو از بیرون به Task داخلی جلوگیری می‌کند، نه از لغو شدنِ خود نقطهٔ await.

یک مثال واقعی: خاموشی ایمن هنگام پرداخت

فرض کنید سرویسی دارید که هنگام خاموشی همهٔ درخواست‌های در‌حال‌اجرا را لغو می‌کند. اما یک تراکنش پرداخت که از درگاه پاسخ گرفته، نباید نیمه‌کاره رها شود:

import asyncio

async def finalize_payment(order):
    await asyncio.sleep(2)            # confirm with the payment gateway
    print(f'order {order}: payment finalized')

async def handle_request(order):
    pay = asyncio.create_task(finalize_payment(order))
    await asyncio.shield(pay)          # survives a shutdown-triggered cancel

async def main():
    req = asyncio.create_task(handle_request(42))
    await asyncio.sleep(0.5)
    req.cancel()                      # graceful shutdown signal
    try:
        await req
    except asyncio.CancelledError:
        print('request cancelled during shutdown')
    await asyncio.sleep(2)            # let the shielded payment complete

asyncio.run(main())

پیتفال: shield نتیجه را نگه نمی‌دارد

مهم‌ترین دامی که باید بشناسید این است: shield کردن یک Task به این معنا نیست که نتیجه‌اش حتماً مصرف می‌شود. اگر پس از دریافت CancelledError دیگر منتظر db_task نمانید، نتیجهٔ نهایی نادیده گرفته می‌شود. بدتر اینکه اگر حلقهٔ رویداد پیش از اتمام آن بسته شود، خودِ Task محافظت‌شده هم لغو می‌شود. دلیلش این است که shield فقط انتشار لغو را متوقف می‌کند، نه اینکه Task را تا ابد زنده نگه دارد. پس بهتر است مرجعی به db_task نگه دارید و در جایی امن منتظرش بمانید.

کی از shield استفاده کنیم؟

از asyncio.shield() زمانی استفاده کنید که یک عملیات باید حتی در صورت لغو شدن Task بیرونی کامل شود: نوشتن در فایل یا دیتابیس، تراکنش پرداخت، یا آزاد کردن منابع. اما آن را بی‌دریغ دور همه‌چیز نپیچید؛ اگر فقط می‌خواهید یک تایم‌اوت بگذارید، asyncio.wait_for مناسب‌تر است، و برای پاک‌سازی هنگام لغو، بلوک finally یا مدیر زمینه راه ساده‌تری است.

جمع‌بندی

  • asyncio.shield() یک Task داخلی را از انتشار لغوی بیرونی در امان نگه می‌دارد؛ لغو فقط به نقطهٔ await می‌رسد.
  • در کدی که shield را await می‌کند، هنوز باید CancelledError را مدیریت کنید.
  • shield نتیجه را نگه نمی‌دارد؛ مرجع Task را نگه دارید و در جایی امن منتظرش بمانید، وگرنه با بسته شدن حلقه لغو می‌شود.
  • برای تایم‌اوت سراغ wait_for بروید؛ shield مخصوص محافظت از عملیات حساس در برابر لغو است.
Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (66%)
نظرات

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

66%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

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

پاسخ تمرین ها

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

اشتراک گذاری

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

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

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

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

تنظیمات

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