پشتیبانی Asynchronous در Django: راهنمای کامل Async Views، ORM، Middleware، عملکرد و ایمنی

این مقاله پشتیبانی Django از برنامه‌نویسی غیرهم‌زمان (async) را توضیح می‌دهد. موضوعاتی مانند async views، اجرای Django تحت ASGI، رفتار middleware، قابلیت‌های ORM غیرهم‌زمان، sync_to_async، مدیریت قطع اتصال، نکات عملکردی، و مکانیزم‌های ایمنی Django برای جلوگیری از استفادهٔ ناامن async بررسی می‌شوند.

Django async, ASGIasync views, sync_to_async, async ORM, asyncioSynchronousOnlyOperation, DJANGO_ALLOW_ASYNC_UNSAFE

~6 دقیقه مطالعه • بروزرسانی ۲۴ اسفند ۱۴۰۴

مقدمه

Django از نسخه‌های اخیر خود پشتیبانی گسترده‌ای از برنامه‌نویسی غیرهم‌زمان (async) ارائه می‌دهد. اگر پروژه را تحت ASGI اجرا کنید، کل زنجیرهٔ پردازش درخواست می‌تواند async باشد. در حالت WSGI نیز async views کار می‌کنند، اما با محدودیت‌های عملکردی.

ORM و برخی بخش‌های Django هنوز کاملاً async نشده‌اند، اما با استفاده از sync_to_async() می‌توان از آن‌ها در محیط async استفاده کرد.


Async Views

هر view در Django می‌تواند async باشد. کافی است آن را با async def تعریف کنید:


async def my_view(request):
    return HttpResponse("Hello async world!")

در class-based views، متدهایی مثل get() و post() باید async باشند.

Django از iscoroutinefunction برای تشخیص async بودن view استفاده می‌کند. اگر روش سفارشی دارید، از markcoroutinefunction استفاده کنید.

Async Views تحت WSGI

در WSGI، هر async view در یک event loop جداگانه اجرا می‌شود. این یعنی async کار می‌کند، اما مزایای ASGI را ندارد.

Async Views تحت ASGI

اینجاست که قدرت async مشخص می‌شود:

  • پشتیبانی از هزاران اتصال هم‌زمان
  • long-polling
  • streaming responses
  • درخواست‌های طولانی بدون استفاده از thread

Middleware و async

برای داشتن یک stack کاملاً async، تمام middleware باید async-capable باشند. اگر حتی یک middleware synchronous باشد، Django مجبور است برای هر درخواست یک thread ایجاد کند.

برای بررسی این موضوع، لاگ django.request را فعال کنید و پیام‌های زیر را جستجو کنید:


"Asynchronous handler adapted for middleware ..."

فراخوانی کد synchronous از async

بخش‌هایی از Django مانند ORM هنوز sync هستند. برای استفادهٔ امن از آن‌ها در async view باید از sync_to_async() استفاده کنید:


from asgiref.sync import sync_to_async

result = await sync_to_async(sync_function, thread_sensitive=True)(pk=123)

اگر کد sync را مستقیم در async فراخوانی کنید، Django خطای SynchronousOnlyOperation می‌دهد.


Decoratorهای سازگار با async

بسیاری از decoratorهای Django با async views سازگارند، از جمله:

  • never_cache()
  • csrf_exempt()
  • require_GET()
  • etag()
  • last_modified()
  • vary_on_headers()
  • xframe_options_deny()
  • و بسیاری دیگر

ORM غیرهم‌زمان

Django از نسخه‌های جدید، ORM async را نیز پشتیبانی می‌کند:


async for author in Author.objects.filter(name__startswith="A"):
    book = await author.books.afirst()

ویژگی‌های مهم

  • تمام متدهای query که SQL اجرا می‌کنند نسخهٔ async با پیشوند a دارند
  • async for روی تمام QuerySetها پشتیبانی می‌شود
  • متدهای async مدل مانند asave() و aset()

محدودیت‌ها

  • تراکنش‌ها در async پشتیبانی نمی‌شوند
  • CONN_MAX_AGE باید غیرفعال شود
  • برای pooling از ابزارهای خارجی استفاده کنید

عملکرد (Performance)

هر بار که Django مجبور به تغییر context بین sync و async شود، حدود ۱ میلی‌ثانیه overhead ایجاد می‌شود.

این اتفاق زمانی رخ می‌دهد که:

  • async view تحت WSGI اجرا شود
  • middleware sync بین ASGI و async view قرار گیرد
  • middleware async روی view sync اعمال شود

در برخی پروژه‌ها حتی کد کاملاً sync نیز تحت ASGI سریع‌تر اجرا می‌شود.


مدیریت قطع اتصال (Disconnect)

در درخواست‌های طولانی، ممکن است کاربر قبل از پایان view قطع شود. در این حالت asyncio.CancelledError رخ می‌دهد:


async def my_view(request):
    try:
        ...
    except asyncio.CancelledError:
        # cleanup
        raise

Async Safety

بخش‌هایی از Django async-safe نیستند و در صورت استفادهٔ اشتباه خطای SynchronousOnlyOperation می‌دهند.

راه‌حل

  • کد sync را در یک تابع sync جداگانه بنویسید
  • آن را با sync_to_async() فراخوانی کنید

محیط‌هایی که ناخواسته async هستند

Jupyter و IPython به‌طور پیش‌فرض event loop فعال دارند. برای غیرفعال کردن:


%autoawait off

برای فعال‌سازی دوباره:


%autoawait on

DJANGO_ALLOW_ASYNC_UNSAFE

اگر مجبور باشید sync code را در async اجرا کنید، می‌توانید این هشدار را غیرفعال کنید:


import os
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

هشدار: این کار در محیط‌های concurrent می‌تواند باعث فساد داده شود. هرگز در production استفاده نکنید.


جمع‌بندی

پشتیبانی async در Django امکان ساخت برنامه‌های بسیار مقیاس‌پذیر و هم‌زمان را فراهم می‌کند—به‌ویژه تحت ASGI. با async views، ORM غیرهم‌زمان، و استفادهٔ صحیح از sync_to_async() می‌توانید سیستم‌هایی مدرن و کارآمد بسازید. با گسترش پشتیبانی async در Django، آیندهٔ این قابلیت‌ها روشن‌تر خواهد شد.

مقدمه

در پروژه‌های Django که از async استفاده می‌کنند، معمولاً نیاز دارید کد synchronous را از محیط async فراخوانی کنید یا برعکس. برای این کار، Django (از طریق کتابخانهٔ asgiref) دو تابع کلیدی ارائه می‌دهد:

  • async_to_sync()
  • sync_to_async()

این توابع ستون فقرات سازگاری بین async و sync در Django هستند.


async_to_sync()

تابع async_to_sync یک تابع async را گرفته و آن را به یک تابع synchronous تبدیل می‌کند.

نمونه استفاده


from asgiref.sync import async_to_sync

async def get_data():
    ...

sync_get_data = async_to_sync(get_data)

@async_to_sync
async def get_other_data():
    ...

نحوهٔ کار

  • اگر event loop فعال باشد، تابع async در همان loop اجرا می‌شود.
  • اگر event loop وجود نداشته باشد، یک loop جدید ساخته و پس از اجرا بسته می‌شود.
  • تابع async همیشه در یک thread جداگانه اجرا می‌شود.
  • مقادیر threadlocal و contextvars در هر دو جهت حفظ می‌شوند.

این تابع نسخهٔ قدرتمندتر asyncio.run() است و با sync_to_async() نیز سازگاری کامل دارد.


sync_to_async()

تابع sync_to_async یک تابع synchronous را گرفته و آن را به یک تابع async تبدیل می‌کند.

نمونه استفاده


from asgiref.sync import sync_to_async

async_function = sync_to_async(sync_function, thread_sensitive=False)

@sync_to_async
def sync_function():
    ...

حفظ threadlocal و contextvars

مانند async_to_sync، این تابع نیز مقادیر threadlocal را در هر دو جهت حفظ می‌کند.

دو حالت threading

  • thread_sensitive=True (پیش‌فرض): تابع sync در همان thread اجرا می‌شود که سایر توابع thread_sensitive اجرا می‌شوند (معمولاً main thread).
  • thread_sensitive=False: تابع sync در یک thread جدید اجرا می‌شود که پس از پایان کار بسته می‌شود.

هشدار: از نسخهٔ 3.3.0 asgiref مقدار پیش‌فرض thread_sensitive برابر True شده است.

چرا thread_sensitive مهم است؟

زیرا بسیاری از کتابخانه‌ها (مثل درایورهای دیتابیس) باید در همان thread که ساخته شده‌اند اجرا شوند. همچنین بخش زیادی از کد Django فرض می‌کند که در یک thread ثابت اجرا می‌شود.

به همین دلیل sync_to_async تضمین می‌کند که تمام کد sync در یک thread مشترک اجرا شود.


نکتهٔ مهم: عدم ارسال connection به sync_to_async

نباید اشیاء حساس به thread مانند connection دیتابیس را مستقیماً به sync_to_async بدهید:


await sync_to_async(connection.cursor)()

این کار باعث خطا می‌شود:

  • SynchronousOnlyOperation در محیط async
  • DatabaseError به دلیل استفاده از connection در thread اشتباه

راه‌حل صحیح

تمام عملیات دیتابیس را داخل یک تابع sync قرار دهید و فقط آن تابع را با sync_to_async فراخوانی کنید.


جمع‌بندی

توابع async_to_sync و sync_to_async ابزارهای حیاتی برای اجرای ایمن و سازگار کد sync و async در Django هستند. این توابع امکان استفادهٔ هم‌زمان از ORM، middleware، و async views را فراهم می‌کنند بدون اینکه با مشکلات thread یا event loop مواجه شوید.

با درک صحیح این دو تابع، می‌توانید پروژه‌های Django را به‌صورت ترکیبی (sync + async) یا کاملاً async توسعه دهید.

نوشته و پژوهش شده توسط دکتر شاهین صیامی