Асинхронные представления в Django 3.1

Spread the love

Перевод статьи: Jace MedlinAsync Views in Django 3.1

Написание асинхронного кода дает возможность значительно ускорить работу приложения. Django 3.1 наконец-то поддерживает асинхронные представления, middleware и тесты, и теперь самое время попробовать их в действие.

В этом посте я расскажу, как начать работу с новыми асинхронными представлениями Django.

Если вам интересно узнать больше о силе асинхронного кода, а также о различиях между потоками, многопроцессорностью и асинхронностью в Python, ознакомьтесь с моей публикацией «Speeding Up Python with Concurrency, Parallelism, and asyncio».

Цели

К концу этого поста вы сможете:

  1. Написать асинхронное представление в Django
  2. Сделайте неблокирующий HTTP-запрос в представлении Django
  3. Упростить основные фоновые задачи с помощью асинхронных представлений Django
  4. Используйте sync_to_async, чтобы сделать синхронный вызов внутри асинхронного представления
  5. Объяснить, когда следует и не следует использовать асинхронные представления

Вы также сможете ответить на следующие вопросы:

  1. Что произойдет, если вы сделаете синхронный вызов внутри асинхронного представления?
  2. Что, если вы сделаете синхронный и асинхронный вызовы внутри асинхронного представления?
  3. Будет ли Celery по-прежнему необходим для асинхронных представлений Django?

Предварительные условия

Если вы уже знакомы с самим Django, добавление асинхронной функциональности в представления чрезвычайно просто.

Зависимости

  1. Python >= 3.8
  2. Django >= 3.1
  3. Uvicorn
  4. HTTPX

Что такое ASGI?

ASGI означает интерфейс шлюза асинхронного сервера (Asynchronous Server Gateway Interface). Это современное асинхронное продолжение WSGI, обеспечивающее стандарт для создания асинхронных веб-приложений на основе Python.

Также стоит упомянуть, что ASGI обратно совместим с WSGI, что упрощает переход с сервера WSGI, такого как Gunicorn или uWSGI, на сервер ASGI, такой как Uvicorn или Daphne, даже если вы не готовы переключиться на написание асинхронных приложений прямо сейчас.

Создание приложения

Создайте новый каталог проекта вместе с новым проектом Django:

$ mkdir django-async-views && cd django-async-views
$ python3.8 -m venv env
$ source env/bin/activate

(env)$ pip install django
(env)$ django-admin.py startproject hello_async .


Не стесняйтесь использовать вместо virtualenv и Pip что то типа Poetry или Pipenv.

Django будет запускать ваши асинхронные представления, даже если вы используете встроенный сервер разработки, но на самом деле они не будут работать асинхронно, поэтому мы будем использовать Uvicorn для асинхронной работы.

Установите его:

(env)$ pip install uvicorn

Чтобы запустить свой проект с помощью Uvicorn, используйте следующую команду из корня вашего проекта:

uvicorn {name of your project}.asgi:application

В нашем случае это будет:

(env)$ uvicorn hello_async.asgi:application

Затем давайте создадим наше первое асинхронное представление. Добавьте новый файл для хранения ваших представлений в папке hello_async, а затем добавьте следующее представление:

# hello_async/views.py

from django.http import HttpResponse


async def index(request):
    return HttpResponse("Hello, async Django!")


Создать асинхронные представления в Django так же просто, как создать синхронное представление – все, что вам нужно сделать, это добавить ключевое слово async.

Обновите URL-адреса:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index


urlpatterns = [
    path("admin/", admin.site.urls),
    path("", index),
]


Теперь в терминале в корневой папке запустите:

(env)$ uvicorn hello_async.asgi:application --reload

Флаг –reload указывает uvicorn следить за вашими файлами на предмет изменений и перезагружать их, если они обнаруживаются. Это, вероятно, не требует пояснений.

Откройте http://localhost:8000/ в своем любимом веб-браузере:

Hello, async Django!

Не самое захватывающее в мире, но, эй, это только начало. Стоит отметить, что запуск этого представления со встроенным сервером разработки Django приведет к точно таким же функциональным возможностям и результатам. Это потому, что мы фактически не делаем ничего асинхронного в обработчике.

HTTPX

Стоит отметить, что поддержка async полностью обратно совместима, поэтому вы можете смешивать представления async и sync, middleware и тесты. Django выполнит каждый из них в соответствующем контексте выполнения.

Чтобы продемонстрировать это, добавьте несколько новых представлений:

# hello_async/views.py

import asyncio
from time import sleep

import httpx
from django.http import HttpResponse


# helpers

async def http_call_async():
    for num in range(1, 6):
        await asyncio.sleep(1)
        print(num)
    async with httpx.AsyncClient() as client:
        r = await client.get("https://httpbin.org/")
        print(r)


def http_call_sync():
    for num in range(1, 6):
        sleep(1)
        print(num)
    r = httpx.get("https://httpbin.org/")
    print(r)


# views

async def index(request):
    return HttpResponse("Hello, async Django!")


async def async_view(request):
    loop = asyncio.get_event_loop()
    loop.create_task(http_call_async())
    return HttpResponse("Non-blocking HTTP request")


def sync_view(request):
    http_call_sync()
    return HttpResponse("Blocking HTTP request")


Обновите URL-адреса:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view


urlpatterns = [
    path("admin/", admin.site.urls),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]


Установим HTTPX:

(env)$ pip install httpx

На работающем сервере перейдите по адресу http://localhost:8000/async/. Вы должны сразу увидеть ответ:

Non-blocking HTTP request

В вашем терминале вы должны увидеть:

INFO:     127.0.0.1:60374 - "GET /async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>


Здесь ответ HTTP отправляется обратно перед первым вызовом sleep.

Затем перейдите по адресу http://localhost:8000/sync/. Получение ответа должно занять около пяти секунд:

Blocking HTTP request

Обратимся к терминалу:

1
2
3
4
5
<Response [200 OK]>
INFO:     127.0.0.1:60375 - "GET /sync/ HTTP/1.1" 200 OK


Здесь ответ HTTP отправляется после цикла, а запрос на https://httpbin.org/ завершается.

Покоптим немного мяса

Теперь давайте напишем представление, которое запускает простую задачу в фоновом режиме.

Вернувшись в URLconf вашего проекта, создайте новый путь в smoke_some_meats:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]


Вернувшись в свои представления, создайте новую асинхронную функцию под названием smoke. Эта функция принимает два параметра: список строк с именем smokables и строку с именем flavor. По умолчанию это список пустой и flavor имеет значение «Sweet Baby Ray’s» соответственно.

# hello_async/views.py

async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> None:
    """ Smokes some meats and applies the Sweet Baby Ray's """

    if smokables is None:
        smokables = [
            "ribs",
            "brisket",
            "lemon chicken",
            "salmon",
            "bison sirloin",
            "sausage",
        ]

    if (loved_smokable := smokables[0]) == "ribs":
        loved_smokable = "meats"

    for smokable in smokables:
        print(f"Smoking some {smokable}....")
        await asyncio.sleep(1)
        print(f"Applying the {flavor}....")
        await asyncio.sleep(1)
        print(f"{smokable.capitalize()} smoked.")

    print(f"Who doesn't love smoked {loved_smokable}?")


Первая строка функции инстанцирует список мяса по умолчанию, если список не задан. Второй оператор «if» затем устанавливает переменную с именем loved_smokable для первого объекта в smokables, если первый объект не является «ribs». Цикл for асинхронно применяет flavor (читай: Sweet Baby Ray) к smokables (читай: smoked meats).

Не забудьте про импорт:

from typing import List


List используется для дополнительных возможностей набора текста. Это не обязательно, и его можно легко пропустить (просто исключите: List [str] после объявления параметра “smokables”).

Затем создайте асинхронное представление, которое использует функцию async smoke:

# hello_async/views.py

async def smoke_some_meats(request) -> HttpResponse:
    loop = asyncio.get_event_loop()
    smoke_args = []

    if to_smoke := request.GET.get("to_smoke"):
        # Grab smokables
        to_smoke = to_smoke.split(",")
        smoke_args += [[smokable.lower().strip() for smokable in to_smoke]]

        # Do some string prettification
        if (smoke_list_len := len(to_smoke)) == 2:
            to_smoke = " and ".join(to_smoke)
        elif smoke_list_len > 2:
            to_smoke[-1] = f"and {to_smoke[-1]}"
            to_smoke = ", ".join(to_smoke)

    else:
        to_smoke = "meats"

    if flavor := request.GET.get("flavor"):
        smoke_args.append(flavor)

    loop.create_task(smoke(*smoke_args))

    return HttpResponse(f"Smoking some {to_smoke}....")


Это представление принимает необязательные параметры запроса to_smoke и flavor. to_smoke – это список мяса, которое нужно закоптить, через запятую, а flavor – это вкус, кторый вы к ним применяете.

Первое, что делает это представление (чего нельзя сделать в стандартном представлении синхронизации) – захватывает цикл событий с помощью asyncio.get_event_loop(). Затем он анализирует параметры запроса, если применимо (и выполняет некоторую очистку строк для окончательного оператора print). Если мы не передаем ничего для копчения, to_smoke по умолчанию принимает значение «meats». Наконец, возвращается ответ, чтобы сообщить пользователю, что он готовит вкусное барбекю.

Отлично. Сохраните файл, затем вернитесь в свой браузер и перейдите по адресу http://localhost:8000/smoke_some_meats/. Вас должен встретить ответ:

Smoking some meats....

В консоли вы должны увидеть:

Smoking some ribs....
INFO:     127.0.0.1:56239 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK

Applying the Sweet Baby Ray's....
Ribs smoked.
Smoking some brisket....
Applying the Sweet Baby Ray's....
Brisket smoked.
Smoking some lemon chicken....
Applying the Sweet Baby Ray's....
Lemon chicken smoked.
Smoking some salmon....
Applying the Sweet Baby Ray's....
Salmon smoked.
Smoking some bison sirloin....
Applying the Sweet Baby Ray's....
Bison sirloin smoked.
Smoking some sausage....
Applying the Sweet Baby Ray's....
Sausage smoked.
Who doesn't love smoked meats?


Обратите внимание, как ребра (ribs) начали коптиться до того, как был зарегистрирован ответ 200. Это асинхронность в действии: поскольку функция smoke изначально спит на одну секунду, представление завершает обработку и возвращает ответ. Конечный пользователь увидит ответ, как только мясо начнет коптиться.

Также стоит отметить, что если вы используете сервер разработки Django, ваш сервер вернет ответ без ошибок, но ничего асинхронного просто не произойдет. Вот как будет выглядеть журнал вашей консоли:

Smoking some ribs....
[16/Aug/2020 22:37:03] "GET /smoke_some_meats/ HTTP/1.1" 200 22

Используя Uvicorn, мы также можем протестировать, используя параметры запроса. Попробуйте http://localhost:8000/smoke_some_meats?to_smoke=ice cream, bananas, cheese&flavor=Gold Bond Medicated Powder. (пробелы будут преобразованы автоматически)

Браузер:

Smoking some ice cream, bananas, and cheese....

Терминал:

Smoking some ice cream....
INFO:     127.0.0.1:56407 - "GET /smoke_some_meats/?to_smoke=ice%20cream,%20bananas,%20cheese&flavor=Gold%20Bond%20Medicated%20Powder HTTP/1.1" 200 OK
Applying the Gold Bond Medicated Powder....
Ice cream smoked.
Smoking some bananas....
Applying the Gold Bond Medicated Powder....
Bananas smoked.
Smoking some cheese....
Applying the Gold Bond Medicated Powder....
Cheese smoked.
Who doesn't love smoked ice cream?


Сгоревшее мясо

Синхронизация вызова

Q: Что, если вы сделаете синхронный вызов внутри асинхронного представления?

То же самое, что произошло бы, если бы вы вызывали неасинхронную функцию из неасинхронного представления.

Чтобы проиллюстрировать это, создайте новую вспомогательную функцию в файле views.py с именем oversmoke:

# hello_async/views.py

def oversmoke() -> None:
    """ If it's not dry, it must be uncooked """
    sleep(5)
    print("Who doesn't love burnt meats?")


Очень просто: мы просто синхронно ждем пять секунд.

Создайте представление, которое вызывает эту функцию:

# hello_async/views.py

async def burn_some_meats(request):
    oversmoke()
    return HttpResponse(f"Burned some meats.")


Наконец, подключите маршрут в URLconf вашего проекта:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import index, async_view, sync_view, smoke_some_meats, burn_some_meats


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]


Посетите сайт в браузере по адресу http://localhost:8000/burn_some_meats:

Burned some meats.

Обратите внимание, как потребовалось пять секунд, чтобы наконец получить ответ от браузера. Вы также должны были одновременно получить вывод консоли:

Who doesn't love burnt meats?
INFO:     127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK

Возможно, стоит отметить, что одно и то же произойдет независимо от того, какой сервер вы используете, будь то WSGI или ASGI.

Синхронизация и асинхронные вызовы

Q: Что, если вы сделаете синхронный и асинхронный вызовы внутри асинхронного представления?

Синхронные и асинхронные представления лучше всего подходят для соответствующих целей. Если у вас есть функция блокировки в асинхронном представлении, в лучшем случае это будет не лучше, чем просто использование синхронного представления. Поэтому в этом нет смысла и этого лучше не делать.

sync_to_async

Если вам нужно сделать синхронный вызов внутри асинхронного представления (например, для взаимодействия с базой данных через ORM Django), используйте sync_to_async как оболочку или декоратор.

Пример:

# hello_async/views.py

async def async_with_sync_view(request):
    loop = asyncio.get_event_loop()
    async_function = sync_to_async(http_call_sync)
    loop.create_task(async_function())
    return HttpResponse("Non-blocking HTTP request (via sync_to_async)")


Не забудьте добавить импорт:

from asgiref.sync import sync_to_async


Добавьте URL:

# hello_async/urls.py

from django.contrib import admin
from django.urls import path

from hello_async.views import (
    index,
    async_view,
    sync_view,
    smoke_some_meats,
    burn_some_meats,
    async_with_sync_view
)


urlpatterns = [
    path("admin/", admin.site.urls),
    path("smoke_some_meats/", smoke_some_meats),
    path("burn_some_meats/", burn_some_meats),
    path("sync_to_async/", async_with_sync_view),
    path("async/", async_view),
    path("sync/", sync_view),
    path("", index),
]


Проверьте это в своем браузере по адресу http://localhost:8003/sync_to_async/

В вашем терминале вы должны увидеть:

INFO:     127.0.0.1:61365 - "GET /sync_to_async/ HTTP/1.1" 200 OK
1
2
3
4
5
<Response [200 OK]>


При использовании sync_to_async блокирующий синхронный вызов обрабатывался в фоновом потоке, что позволяло отправлять HTTP-ответ перед первым вызовом sleep.

Celery и асинхронные представления

Q: Необходим ли Celery для асинхронных представлений Django?

Это зависит….

Асинхронные представления Django предлагают те же функции, что и задачи или очередь сообщений. Если вы используете (или рассматриваете) Django и хотите сделать что-то простое (например, отправить электронное письмо новому подписчику или вызвать внешний API), асинхронные представления – отличный способ сделать это быстро и легко. Если вам нужно выполнять гораздо более тяжелые и длительные фоновые процессы, вы все равно захотите использовать Celery или RQ.

Следует отметить, что для эффективного использования асинхронных представлений в представлении должны быть только асинхронные вызовы. Очереди задач, с другой стороны, используют рабочие процессы в отдельных процессах и, следовательно, могут выполнять синхронные вызовы в фоновом режиме на нескольких серверах.

Кстати, вы ни в коем случае не должны выбирать между асинхронными представлениями и очередью сообщений – вы можете легко использовать их в тандеме. Например: вы можете использовать асинхронное представление, чтобы отправить электронное письмо или внести одноразовую модификацию базы данных, но попросите Celery очищать вашу базу данных в запланированное время каждую ночь или создавать и отправлять отчеты о клиентах.

Когда что использовать

Для проектов с нуля используйте асинхронные представления и напишите свои процессы ввода-вывода максимально асинхронным образом. Тем не менее, если большинству ваших представлений просто нужно совершать вызовы в базу данных и выполнять некоторую базовую обработку перед возвратом данных, вы не увидите значительного увеличения скорости (если таковое имеется) по сравнению с простым использованием синхронных представлений.

Если у вас мало или совсем нет процессов ввода-вывода придерживайтесь синхронизирующих представлений. Если у вас есть несколько процессов ввода-вывода, оцените, насколько легко будет переписать их асинхронным способом. Переписать синхронизирующий ввод-вывод в асинхронный режим непросто, поэтому вы, вероятно, захотите оптимизировать синхронизирующий ввод-вывод и представления, прежде чем пытаться переписать его в асинхронный режим. Кроме того, никогда не стоит смешивать синхронные процессы с вашими асинхронными представлениями.

В производственной среде обязательно используйте Gunicorn для управления Uvicorn, чтобы воспользоваться преимуществами параллелизма (через Uvicorn) и параллелизма (через рабочие процессы Gunicorn):

gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application

Заключение

В заключение, хотя это был простой вариант использования, он должен дать вам приблизительное представление о возможностях, которые открывают новые асинхронные представления Django. Некоторые другие вещи, которые можно попробовать в ваших асинхронных представлениях, – это отправка электронных писем, вызов сторонних API и запись в файл. Подумайте о тех представлениях в вашем коде, в которых есть простые процессы, которые не обязательно должны возвращать что-либо непосредственно конечному пользователю – их можно быстро преобразовать в асинхронный.

Чтобы узнать больше о новооткрытой асинхронности Django, прочтите этот отличный пост, посвященный той же теме, а также многопоточности и тестированию.

Была ли вам полезна эта статья?
[9 / 4]

Spread the love
Подписаться
Уведомление о
guest
3 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
lexover
lexover
3 лет назад

Перевод статьи “Speeding Up Python With Concurrency, Parallelism and asyncio” – “Разгоняем Python с помощью Конкурентности, Параллелизма и asyncio”

Анонимно
Анонимно
2 лет назад

Не, ну вы серьёзно?
В статье про асинхронные запросы рассказываете как создавать новое приложение django?

Анонимно
Анонимно
1 год назад

налито много воды. Пример с этим мясом, “простая” задача в фоновом режиме – вообще никуда не годится и не читается. Автор явно не ищет простых путей