معرفی contextvars – جایگزینی مدرن برای thread-local در دنیای async
اگر با برنامهنویسی همروند در پایتون کار کرده باشید، حتماً با threading.local آشنا هستید. این ابزار به ما امکان میدهد دادههایی را ذخیره کنیم که مختص هر ترد باشند. اما وقتی وارد دنیای asyncio میشویم، threading.local دیگر پاسخگوی نیاز ما نیست. چرا؟ چون در asyncio، چندین تسک بهصورت همروند در یک ترد واحد اجرا میشوند و threading.local نمیتواند بین این تسکها تمایز قائل شود. در چنین شرایطی، دادههای مربوط به یک تسک ممکن است بهصورت ناخواسته به تسک دیگر نشت کند.
اینجاست که ماژول contextvars که از پایتون ۳.۷ به بعد در دسترس است، به کمک ما میآید.
تفاوت کلیدی contextvars با thread-local
threading.local داده را بر اساس ترد مجزا میکند، در حالی که contextvars داده را بر اساس زمینه (Context) تفکیک میکند. این تفاوت، ظرافت و در عین حال قدرت اصلی contextvars است. در asyncio، هر تسک زمینهی مخصوص به خود را دارد و contextvars تضمین میکند که هر متغیر فقط در همان زمینهای که تعریف شده، در دسترس باشد.
به بیان دقیقتر، contextvars برای کدهای ناهمگام (asynchronous) و مدیریتکنندههای زمینه (Context Managers) که وضعیت داخلی دارند، راهحلی اصولی ارائه میدهد تا از نشت اطلاعات به سایر بخشهای کد جلوگیری کند. خودِ asyncio نیز بهصورت پیشفرض از contextvars پشتیبانی میکند.
کاربردهای رایج contextvars
- ذخیرهسازی دادههای مربوط به یک درخواست (Request-Specific Data): در برنامههای تحت وب، مانند ذخیرهسازی توکن امنیتی یا شناسهی درخواست برای هر کاربر بهصورت مجزا.
- مدیریت وضعیت تسکها (Task-Local State): نگهداری دادههایی که مختص یک تسک خاص در
asyncioهستند، بدون نیاز به پاس دادن آنها بهصورت صریح در تمام توابع. - ثبتوقایع و ردیابی (Logging و Tracing): برای خودکارسازی انتشار متادیتا مانند شناسهی درخواست در تمام مراحل اجرای یک تابع، بدون نیاز به ارسال پارامترهای اضافی.
- مدیریت زمینه در ترکیب ترد و تسک: وقتی کد شما هم از ترد و هم از تسکهای
asyncioاستفاده میکند،contextvarsاز برخورد و نشت دادهها جلوگیری میکند.
آشنایی با مفاهیم پایهای contextvars
برای کار با contextvars، با سه مفهوم اصلی روبرو میشویم:
۱. ContextVar: کلاسی برای تعریف یک متغیر زمینهای. این متغیرها معمولاً در سطح ماژول تعریف میشوند و به عنوان کلید برای ذخیرهسازی داده در زمینههای مختلف عمل میکنند.
۲. Context: نگاشتی از متغیرهای ContextVar به مقادیرشان. هر ترد، پشتهای از زمینهها دارد و زمینهی جاری، بالاترین عضو این پشته است. برای کار با یک زمینه، باید از متد run() بر روی آن استفاده کنید.
۳. copy_context(): تابعی برای گرفتن یک کپی از زمینهی جاری. این کپی را میتوانید به تردهای دیگر بفرستید یا در جای دیگری اجرا کنید.
مثالی از دنیای واقعی
فرض کنید یک وبسرویس داریم که برای هر درخواست کاربر، باید یک شناسهی یکتا ثبت کند تا بتواند لاگهای مرتبط با آن کاربر را ردیابی کند. بدون contextvars، باید این شناسه را بهصورت یک پارامتر به تمام توابعی که لاگ مینویسند، پاس میدادیم. با contextvars، کار بهمراتب تمیزتر میشود:
import asyncio
import contextvars
request_id = contextvars.ContextVar('request_id')
async def handle_request(user):
token = request_id.set(f'req-{user}')
print(f'[{user}] request id set: {request_id.get()}')
await process_user_action(user)
request_id.reset(token)
async def process_user_action(user):
# this function accesses request id without receiving it as parameter
print(f'[{user}] processing for request id: {request_id.get()}')
await asyncio.sleep(0.1)
async def main():
users = ['ali', 'sara', 'reza']
tasks = [asyncio.create_task(handle_request(u)) for u in users]
await asyncio.gather(*tasks)
asyncio.run(main())در این مثال، هر کاربر یک شناسهی منحصربهفرد دریافت میکند و توابع process_user_action و حتی توابعی که عمیقتر فراخوانی میشوند، میتوانند بدون دریافت پارامتر اضافی، به شناسهی مربوط به همان کاربر دسترسی داشته باشند.
یک نکتهی تکمیلی
ممکن است نام extracontext را هم دیده باشید. این یک کتابخانهی شخص ثالث است که سعی دارد رابط کاربری contextvars را به threading.local شبیهتر کند و از دکوراتورها برای ایزولهسازی خودکار زمینه استفاده میکند. اگرچه جذاب است، اما contextvars خودِ پایتون، راهحلی استاندارد، قدرتمند و بهخوبی مستند است که برای اکثر موارد کافی خواهد بود.
بهطور خلاصه، اگر کد asyncio مینویسید و نیاز به نگهداری دادههای مختص هر تسک دارید، contextvars ابزار مناسب شماست. این ماژول، جایگزینی مدرن و کارآمد برای threading.local در دنیای برنامهنویسی ناهمگام است.