非同步支援

Django 支援撰寫非同步(“async”)視圖,並且如果您在 ASGI 下執行,則具有完全啟用非同步的請求堆疊。非同步視圖在 WSGI 下仍然可以運作,但會有效能損失,並且無法有效率地處理長時間執行的請求。

我們仍在努力為 ORM 和 Django 的其他部分提供非同步支援。您可以在未來的版本中看到這些。目前,您可以使用 sync_to_async() 配接器與 Django 的同步部分互動。還有許多您可以整合的非同步原生 Python 函式庫。

非同步視圖

任何視圖都可以透過使其可呼叫部分傳回協程來宣告為非同步 - 通常,這是使用 async def 完成的。對於基於函式的視圖,這表示使用 async def 宣告整個視圖。對於基於類別的視圖,這表示將 HTTP 方法處理器(例如 get()post())宣告為 async def(而不是其 __init__()as_view())。

注意

Django 使用 asgiref.sync.iscoroutinefunction 來測試您的視圖是否為非同步。如果您實作自己的方法來傳回協程,請確保使用 asgiref.sync.markcoroutinefunction,以便此函式傳回 True

在 WSGI 伺服器下,非同步視圖將在其自己的單次事件迴圈中執行。這表示您可以毫無問題地使用非同步功能,例如並行的非同步 HTTP 請求,但您將無法獲得非同步堆疊的好處。

主要好處是在不使用 Python 執行緒的情況下,能夠為數百個連線提供服務。這允許您使用慢速串流、長輪詢和其他令人興奮的回應類型。

如果您想使用這些,您需要改用 ASGI 部署 Django。

警告

只有當您的網站中沒有載入同步中介軟體時,您才能獲得完全非同步請求堆疊的好處。如果有一個同步中介軟體,則 Django 必須為每個請求使用一個執行緒,以便安全地為其模擬同步環境。

中介軟體可以建置為支援 同步和非同步 環境。Django 的某些中介軟體是這樣建置的,但並非全部。若要查看 Django 必須調整哪些中介軟體,您可以開啟 django.request 記錄器的偵錯記錄,並尋找關於「為中介軟體調整的非同步處理器...」的記錄訊息。

在 ASGI 和 WSGI 模式下,您仍然可以安全地使用非同步支援來並行執行程式碼,而不是依序執行。這在處理外部 API 或資料儲存時尤其方便。

如果您想呼叫 Django 中仍然是同步的部分,您需要將其包裝在 sync_to_async() 呼叫中。例如

from asgiref.sync import sync_to_async

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

如果您不小心嘗試從非同步視圖呼叫 Django 中僅限同步的部分,將會觸發 Django 的 非同步安全保護,以保護您的資料免於損毀。

裝飾器

Django 5.0 新增。

下列裝飾器可用於同步和非同步視圖函式

例如

from django.views.decorators.cache import never_cache


@never_cache
def my_sync_view(request): ...


@never_cache
async def my_async_view(request): ...

查詢與 ORM

除了一些例外情況外,Django 也可以非同步執行 ORM 查詢

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

詳細說明可以在 非同步查詢 中找到,但簡而言之

  • 所有會導致發生 SQL 查詢的 QuerySet 方法都有一個以 a 為前綴的非同步變體。

  • 所有 QuerySet(包括 values()values_list() 的輸出)都支援 async for

Django 也支援一些使用資料庫的非同步模型方法

async def make_book(*args, **kwargs):
    book = Book(...)
    await book.asave(using="secondary")


async def make_book_with_tags(tags, *args, **kwargs):
    book = await Book.objects.acreate(...)
    await book.tags.aset(tags)

交易尚未在非同步模式下運作。如果您有一段程式碼需要交易行為,我們建議您將該段程式碼寫成單一同步函式,並使用 sync_to_async() 呼叫它。

效能

當在與視圖不符的模式下執行時(例如,在 WSGI 下的非同步視圖,或在 ASGI 下的傳統同步視圖),Django 必須模擬其他呼叫樣式以允許您的程式碼執行。這種內容切換會導致大約一毫秒的效能損失。

中介軟體也是如此。Django 將嘗試最小化同步和非同步之間的內容切換次數。如果您有一個 ASGI 伺服器,但您的所有中介軟體和視圖都是同步的,則它只會在進入中介軟體堆疊之前切換一次。

但是,如果您在 ASGI 伺服器和非同步視圖之間放置同步中介軟體,則它必須為中介軟體切換到同步模式,然後再為視圖切換回非同步模式。Django 也會保持同步執行緒開啟,以進行中介軟體例外狀況傳播。這可能一開始不會注意到,但是為每個請求新增這個執行緒損失可能會消除任何非同步效能優勢。

您應該進行自己的效能測試,以了解 ASGI 相對於 WSGI 對您的程式碼有何影響。在某些情況下,即使在 ASGI 下的純同步程式碼庫中,也可能會出現效能提升,因為請求處理程式碼仍然全部非同步執行。一般來說,只有在您的專案中有非同步程式碼時,才會想要啟用 ASGI 模式。

處理斷線

Django 5.0 新增。

對於長時間執行的請求,用戶端可能會在視圖傳回回應之前斷線。在這種情況下,視圖中將引發 asyncio.CancelledError。您可以捕獲此錯誤並在需要執行任何清理時處理它

async def my_view(request):
    try:
        # Do some work
        ...
    except asyncio.CancelledError:
        # Handle disconnect
        raise

您也可以 在串流回應中處理用戶端斷線

非同步安全

DJANGO_ALLOW_ASYNC_UNSAFE

Django 的某些關鍵部分無法在非同步環境中安全運作,因為它們具有非協程感知之全域狀態。Django 的這些部分被歸類為「非同步不安全」,並且受到保護而無法在非同步環境中執行。ORM 是主要的例子,但還有其他部分也受到這種方式的保護。

如果您嘗試從具有正在執行之事件迴圈的執行緒執行任何這些部分,您將會收到 SynchronousOnlyOperation 錯誤。請注意,您不必直接位於非同步函式內就會發生此錯誤。如果您在沒有使用 sync_to_async() 或類似方法的情況下,直接從非同步函式呼叫同步函式,也可能發生此錯誤。這是因為您的程式碼仍然在具有作用中事件迴圈的執行緒中執行,即使它可能未宣告為非同步程式碼。

如果您遇到此錯誤,您應該修正程式碼,使其不會從非同步內容呼叫違規程式碼。相反地,請將與非同步不安全函式對話的程式碼寫在其自己的同步函式中,並使用 asgiref.sync.sync_to_async()(或任何其他在自己的執行緒中執行同步程式碼的方式)呼叫它。

非同步上下文可能會因為您執行 Django 程式碼的環境而強制套用。例如,Jupyter 筆記本和 IPython 互動式 shell 都會透明地提供一個活動的事件迴圈,以便更容易與非同步 API 互動。

如果您正在使用 IPython shell,您可以透過執行以下命令來停用此事件迴圈:

%autoawait off

在 IPython 提示符下執行此命令。這將允許您執行同步程式碼,而不會產生 SynchronousOnlyOperation 錯誤;但是,您也無法 await 非同步 API。要重新啟用事件迴圈,請執行:

%autoawait on

如果您在 IPython 以外的環境中(或者由於某些原因無法在 IPython 中關閉 autoawait),並且您確定您的程式碼不會同時執行,而且您絕對需要在非同步上下文中執行同步程式碼,那麼您可以將 DJANGO_ALLOW_ASYNC_UNSAFE 環境變數設定為任何值,來停用警告。

警告

如果您啟用此選項,並且同時存取 Django 的非同步不安全部分,您可能會遭受資料遺失或損毀。請非常小心,不要在生產環境中使用此選項。

如果您需要從 Python 內部執行此操作,請使用 os.environ 來執行。

import os

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

非同步轉接函式

從非同步上下文呼叫同步程式碼或反之亦然時,必須調整呼叫方式。為此,asgiref.sync 模組提供了兩個轉接函式:async_to_sync()sync_to_async()。它們用於在保留相容性的同時,在呼叫方式之間轉換。

這些轉接函式在 Django 中被廣泛使用。asgiref 套件本身是 Django 專案的一部分,當您使用 pip 安裝 Django 時,它會自動作為依賴項安裝。

async_to_sync()

async_to_sync(async_function, force_new_loop=False)

接收一個非同步函式,並返回一個包裹它的同步函式。可以用作直接包裝器或裝飾器。

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(): ...

如果當前執行緒存在事件迴圈,則非同步函式會在該事件迴圈中執行。如果沒有當前事件迴圈,則會專門為單個非同步調用啟動一個新的事件迴圈,並在完成後再次關閉。在任何情況下,非同步函式都將在與呼叫程式碼不同的執行緒上執行。

Threadlocals 和 contextvars 值在兩個方向上都會跨邊界保留。

async_to_sync() 本質上是 Python 標準函式庫中 asyncio.run() 函式的更強大版本。除了確保 threadlocals 正常工作外,當在它之下使用包裝器時,它還啟用了 sync_to_async()thread_sensitive 模式。

sync_to_async()

sync_to_async(sync_function, thread_sensitive=True)

接收一個同步函式,並返回一個包裹它的非同步函式。可以用作直接包裝器或裝飾器。

from asgiref.sync import sync_to_async

async_function = sync_to_async(sync_function, thread_sensitive=False)
async_function = sync_to_async(sensitive_sync_function, thread_sensitive=True)


@sync_to_async
def sync_function(): ...

Threadlocals 和 contextvars 值在兩個方向上都會跨邊界保留。

同步函式在編寫時通常會假設它們都在主執行緒中執行,因此 sync_to_async() 有兩種執行緒模式。

  • thread_sensitive=True (預設):同步函式將在與所有其他 thread_sensitive 函式相同的執行緒中執行。如果主執行緒是同步的,並且您正在使用 async_to_sync() 包裝器,則這將是主執行緒。

  • thread_sensitive=False:同步函式將在一個全新的執行緒中執行,該執行緒在調用完成後關閉。

警告

asgiref 版本 3.3.0 將 thread_sensitive 參數的預設值變更為 True。這是一個更安全的預設值,並且在許多情況下與 Django 互動時是正確的值,但如果從較早的版本更新 asgiref,請務必評估 sync_to_async() 的使用情況。

執行緒敏感模式非常特殊,並且需要執行大量工作才能在同一執行緒中執行所有函式。但是請注意,它依賴於堆疊上方使用 async_to_sync(),才能在主執行緒上正確執行操作。如果您使用 asyncio.run() 或類似的東西,它將會退回在單個共享執行緒中執行執行緒敏感函式,但這將不是主執行緒。

在 Django 中需要這樣做的原因是,許多函式庫(特別是資料庫轉接器)要求它們在其建立所在的同一執行緒中被存取。此外,許多現有的 Django 程式碼都假設它們都在同一執行緒中執行,例如,中間件會將內容添加到請求中,以供稍後在視圖中使用。

我們沒有引入此程式碼的潛在相容性問題,而是選擇添加此模式,以便所有現有的 Django 同步程式碼都在同一執行緒中執行,從而與非同步模式完全相容。請注意,同步程式碼始終會在與呼叫它的任何非同步程式碼不同的執行緒中執行,因此您應避免傳遞原始資料庫句柄或其他執行緒敏感的參考。

在實踐中,這種限制意味著您在呼叫 sync_to_async() 時,不應傳遞資料庫 connection 物件的功能。這樣做會觸發執行緒安全性檢查。

# DJANGO_SETTINGS_MODULE=settings.py python -m asyncio
>>> import asyncio
>>> from asgiref.sync import sync_to_async
>>> from django.db import connection
>>> # In an async context so you cannot use the database directly:
>>> connection.cursor()
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from
an async context - use a thread or sync_to_async.
>>> # Nor can you pass resolved connection attributes across threads:
>>> await sync_to_async(connection.cursor)()
django.db.utils.DatabaseError: DatabaseWrapper objects created in a thread
can only be used in that same thread. The object with alias 'default' was
created in thread id 4371465600 and this is thread id 6131478528.

相反,您應該將所有資料庫存取封裝在一個輔助函式中,該函式可以使用 sync_to_async() 呼叫,而無需依賴呼叫程式碼中的連線物件。

返回頂部