Асинхронные представления в Django
Написание асинхронного кода дает вам возможность ускорить работу вашего приложения без особых усилий. Версии Django >= 3.1 поддерживают асинхронные представления, промежуточное программное обеспечение и тесты. Если вы еще не экспериментировали с асинхронными представлениями, сейчас самое время освоить их.
В этом руководстве рассказывается о том, как начать работу с асинхронными представлениями Django.
Если вам интересно узнать больше о возможностях асинхронного кода, а также о различиях между потоками, многопроцессорностью и асинхронностью в Python, ознакомьтесь с моей статьей Ускорение работы Python с помощью параллелизма, распараллеливаемости и asyncio статья.
Цели
К концу этого урока вы должны уметь:
- Написать асинхронное представление в Django
- Создать неблокирующий HTTP-запрос в представлении Django
- Упростите основные фоновые задачи с помощью асинхронных представлений Django
- Используйте
sync_to_async
для выполнения синхронного вызова внутри асинхронного представления - Объясните, когда следует, а когда не следует использовать асинхронные представления
Вы также должны быть в состоянии ответить на следующие вопросы:
- Что делать, если вы выполняете асинхронный вызов внутри асинхронного представления?
- Что делать, если вы выполняете синхронный и асинхронный вызовы внутри асинхронного представления?
- Необходим ли Celery по-прежнему для асинхронных представлений Django?
Предварительные требования
Если вы уже знакомы с самим Django, добавить асинхронную функциональность в представления, не основанные на классах, чрезвычайно просто.
Зависимости
- Python >= 3.8
- Джанго >= 3.1
- Uvicorn
- HTTPX
Что такое ASGI?
ASGI расшифровывается как асинхронный серверный шлюзовой интерфейс. Это современное асинхронное дополнение к WSGI, обеспечивающее стандарт для создания асинхронных веб-приложений на основе Python.
Еще стоит упомянуть, что ASGI обратно совместим с WSGI, что является хорошим поводом для перехода с сервера WSGI, такого как Gunicorn или uWSGI, на сервер ASGI, такой как Uvicorn или Дафна даже если вы не готовы перейти к написанию асинхронных приложений.
Создание приложения
Создайте новый каталог проекта вместе с новым проектом Django:
$ mkdir django-async-views && cd django-async-views
$ python3.10 -m venv env
$ source env/bin/activate
(env)$ pip install django
(env)$ django-admin startproject hello_async .
Не стесняйтесь заменить virtualenv и Pip на Poetry или Pipenv. Для получения дополнительной информации ознакомьтесь с Современными средами Python.
Django будет запускать ваши асинхронные представления, если вы используете встроенный сервер разработки, но на самом деле он не будет запускать их асинхронно, поэтому мы запустим Django с помощью Uvicorn.
Установите его:
(env)$ pip install uvicorn
Чтобы запустить свой проект с Unicorn, вы используете следующую команду из корневого каталога вашего проекта:
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
Стоит отметить, что асинхронная поддержка полностью обратно совместима, поэтому вы можете комбинировать асинхронные и синхронизирующие представления, промежуточное программное обеспечение и тесты. 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-ответ отправляется обратно перед первым вызовом режима ожидания.
Затем перейдите к 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
. По умолчанию это список копченого мяса и "Sweet Baby Ray's", соответственно.
# hello_async/views.py
async def smoke(smokables: List[str] = None, flavor: str = "Sweet Baby Ray's") -> List[str]:
""" Smokes some meats and applies the Sweet Baby Ray's """
for smokable in smokables:
print(f"Smoking some {smokable}...")
print(f"Applying the {flavor}...")
print(f"{smokable.capitalize()} smoked.")
return len(smokables)
Цикл for асинхронно применяет аромат (читай: Sweet Baby Ray's) к копченостям (читай: копченому мясу).
Не забудьте импортировать:
from typing import List
List
используется для дополнительных возможностей ввода текста. Это не обязательно и может быть легко опущено (просто уберите: List[str]
после объявления параметра "smokables").
Затем добавьте еще два асинхронных помощника:
async def get_smokables():
print("Getting smokeables...")
await asyncio.sleep(2)
async with httpx.AsyncClient() as client:
await client.get("https://httpbin.org/")
print("Returning smokeable")
return [
"ribs",
"brisket",
"lemon chicken",
"salmon",
"bison sirloin",
"sausage",
]
async def get_flavor():
print("Getting flavor...")
await asyncio.sleep(1)
async with httpx.AsyncClient() as client:
await client.get("https://httpbin.org/")
print("Returning flavor")
return random.choice(
[
"Sweet Baby Ray's",
"Stubb's Original",
"Famous Dave's",
]
)
Обязательно добавьте импорт:
import random
Создайте асинхронное представление, использующее асинхронные функции:
# hello_async/views.py
async def smoke_some_meats(request):
results = await asyncio.gather(*[get_smokables(), get_flavor()])
total = await asyncio.gather(*[smoke(results[0], results[1])])
return HttpResponse(f"Smoked {total[0]} meats with {results[1]}!")
В этом представлении функции get_smokables
и get_flavor
вызываются одновременно. Поскольку smoke
зависит от результатов как get_smokables
, так и get_flavor
, мы использовали gather
для ожидания завершения каждой асинхронной задачи.
Имейте в виду, что в обычном режиме синхронизации get_smokables
и get_flavor
будут обрабатываться по очереди. Кроме того, асинхронное представление обеспечивает выполнение и позволяет обрабатывать другие запросы во время обработки асинхронных задач, что позволяет одному и тому же процессу обрабатывать больше запросов за определенный промежуток времени.
Наконец, возвращается ответ, сообщающий пользователю, что вкусное блюдо для барбекю готово.
Отлично. Сохраните файл, затем вернитесь в свой браузер и перейдите по ссылке http://localhost:8000/smoke_some_meats/. Ответ должен появиться через несколько секунд:
Smoked 6 meats with Sweet Baby Ray's!
В вашей консоли вы должны увидеть:
Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable
Smoking some ribs...
Applying the Stubb's Original...
Ribs smoked.
Smoking some brisket...
Applying the Stubb's Original...
Brisket smoked.
Smoking some lemon chicken...
Applying the Stubb's Original...
Lemon chicken smoked.
Smoking some salmon...
Applying the Stubb's Original...
Salmon smoked.
Smoking some bison sirloin...
Applying the Stubb's Original...
Bison sirloin smoked.
Smoking some sausage...
Applying the Stubb's Original...
Sausage smoked.
INFO: 127.0.0.1:57501 - "GET /smoke_some_meats/ HTTP/1.1" 200 OK
Обратите внимание на порядок следования инструкций для печати:
Getting smokeables...
Getting flavor...
Returning flavor
Returning smokeable
Это и есть асинхронность в работе: когда функция get_smokables
переходит в спящий режим, функция get_flavor
завершает обработку.
Разбор полетов
Синхронизирующий вызов
Вопрос: Что делать, если вы выполняете синхронный вызов внутри асинхронного представления?
То же самое, что произошло бы, если бы вы вызвали несинхронную функцию из несинхронного представления.
--
Чтобы проиллюстрировать это, создайте новую вспомогательную функцию в вашем 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
Возможно, стоит отметить, что то же самое произойдет независимо от используемого вами сервера, будь то на базе SGI или ASGI.
Синхронизация и асинхронные вызовы
Вопрос: Что, если вы сделаете синхронный и асинхронный вызовы внутри асинхронного представления?
Не делай этого.
Синхронные и асинхронные представления, как правило, лучше всего подходят для разных целей. Если у вас есть функция блокировки в асинхронном представлении, в лучшем случае это будет не лучше, чем просто использовать синхронное представление.
Синхронизация в асинхронном режиме
Если вам нужно выполнить синхронный вызов внутри асинхронного представления (например, для взаимодействия с базой данных через Django ORM, например), используйте 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, thread_sensitive=False)
loop.create_task(async_function())
return HttpResponse("Non-blocking HTTP request (via sync_to_async)")
Вы заметили, что мы установили для параметра
thread_sensitive
значениеFalse
? Это означает, что синхронная функцияhttp_call_sync
будет запущена в новом потоке. Просмотрите документы для получения дополнительной информации.
Добавьте импорт в начало:
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:8000/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 обратно до первого вызова в режиме ожидания.
Celery и асинхронные представления
Вопрос: По-прежнему ли необходим Celery для асинхронных представлений Django?
Это зависит от обстоятельств.
Асинхронные представления Django предлагают функциональность, аналогичную задаче или очереди сообщений, но без такой сложности. Если вы используете (или рассматриваете) Django и хотите сделать что-то простое (и не заботитесь о надежности), асинхронные представления - отличный способ сделать это быстро и легко. Если вам нужно выполнять более тяжелые и длительные фоновые процессы, вы все равно захотите использовать Celery или RQ.
Следует отметить, что для эффективного использования асинхронных представлений в представлении должны быть только асинхронные вызовы. Очереди задач, с другой стороны, используют работников в отдельных процессах и, следовательно, способны выполнять синхронные вызовы в фоновом режиме на нескольких серверах.
Кстати, вам ни в коем случае не нужно выбирать между асинхронным просмотром и очередью сообщений - вы можете легко использовать их в паре. Например, вы могли бы использовать асинхронное представление для отправки электронного письма или внесения разовых изменений в базу данных, но попросите Celery очищать вашу базу данных в назначенное время каждую ночь или создавать и отправлять отчеты клиентам.
Когда использовать
Если вам нравится асинхронность в новых проектах, максимально используйте асинхронные представления и создавайте свои процессы ввода-вывода асинхронным способом. Тем не менее, если большинству ваших просмотров нужно просто обращаться к базе данных и выполнять некоторую базовую обработку перед возвратом данных, вы не увидите большого увеличения (если таковое вообще будет) по сравнению с использованием только синхронизированных просмотров.
Если у вас мало процессов ввода-вывода или их вообще нет, используйте синхронизированные представления для новых проектов. Если у вас есть несколько процессов ввода-вывода, оцените, насколько легко будет переписать их асинхронным способом. Переписать синхронизирующий ввод-вывод в асинхронный режим непросто, поэтому, вероятно, вам захочется оптимизировать синхронизирующий ввод-вывод и представления, прежде чем пытаться переписать в асинхронный режим. Кроме того, никогда не рекомендуется смешивать процессы синхронизации с асинхронными представлениями.
В производственной среде обязательно используйте Gunicorn для управления Unicorn, чтобы использовать преимущества как параллелизма (через Unicorn), так и параллелизма (через Gunicorn workers):
gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application
Заключение
В заключение, хотя это был простой пример использования, он должен дать вам приблизительное представление о возможностях, которые открывают асинхронные представления Django. В асинхронных представлениях также можно использовать отправку электронных писем, вызов сторонних API-интерфейсов и чтение из файлов и запись в них.
Подробнее о новообретенной асинхронности Django читайте в этой отличной статье, которая посвящена той же теме, а также многопоточности и тестированию.
Вернуться на верх