编写异步代码能够毫不费力地加速应用程序。 随着Django 3.1最终支持异步视图,中间件和测试,现在是将它们置入现实的好时机。
这篇文章探讨了如何开始使用Django的新异步视图。
如果想了解有关异步代码背后的强大功能以及Python中线程,多处理和异步之间的区别的更多信息,请查看我的《通过并发,并行和异步编写Speeding Up Python》。
目标
在这篇文章的结尾,您应该能够:
在Django中编写异步视图
在Django视图中发出非阻塞HTTP请求
使用Django的异步视图简化基本的后台任务
使用sync_to_async在异步视图内进行同步调用
说明何时应该和不应该使用异步视图
您还应该能够回答以下问题:
如果您在异步视图中进行同步调用怎么办?
如果您在异步视图中进行同步和异步调用怎么办?
Celery是否仍然需要Django的异步视图?
先决条件
只要您已经熟悉Django本身,向非基于类的视图添加异步功能就非常简单。
依存关系
Python> = 3.8
Django> = 3.1
葡萄胎
HTTPX
什么是ASGI?
ASGI代表异步服务器网关接口。 它是WSGI的现代,异步的后续产品,为创建基于Python的异步Web应用程序提供了标准。
值得一提的另一件事是,ASGI与WSGI向后兼容,即使您不准备转向编写异步应用程序,也可以将其从Gunicorn或uWSGI之类的WSGI服务器切换至Uvicorn或Daphne之类的ASGI服务器。 。
创建应用
创建一个新的项目目录以及一个新的Django项目:
1 2 3 4 5 6 |
$ 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 . |
如果使用内置开发服务器,则Django将运行您的异步视图,但实际上不会异步运行它们,因此我们将使用Uvicorn来支撑服务器。
安装它:
1 |
(env)$ pip install uvicorn |
要使用Uvicorn运行项目,请从项目的根目录使用以下命令:
1 |
uvicorn {name of your project}.asgi:application |
我们的例子:
1 |
(env)$ uvicorn hello_async.asgi:application |
接下来,让我们创建第一个异步视图。 添加一个新文件以将视图保存在“ hello_async”文件夹中,然后添加以下视图:
1 2 3 4 5 6 7 |
# hello_async/views.py from django.http import HttpResponse async def index(request): return HttpResponse("Hello, async Django!") |
在Django中创建异步视图就像创建同步视图一样简单-您所需要做的就是添加async关键字。
更新网址:
1 2 3 4 5 6 7 8 9 10 11 12 |
# 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), ] |
终端运行:
1 |
(env)$ uvicorn hello_async.asgi:application --reload |
–reload标志告诉uvicorn监视文件中的更改,如果发现更改,则重新加载。 那可能是不言而喻的。
打开http://127.0.0.1:8000 显示:
1 |
Hello, async Django! |
这不是世界上最令人兴奋的事情,但是,嘿,这是一个开始。 值得注意的是,使用Django的内置开发服务器运行此视图将获得完全相同的功能和输出。 这是因为我们实际上没有在处理程序中执行任何异步操作。
HTTPX
值得注意的是,异步支持是完全向后兼容的,因此可以混合使用异步和同步视图,中间件和测试。 Django将在适当的执行上下文中执行每个。
为了说明这一点,请添加一些新视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# 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") |
更新urls.py文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 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:
1 |
(env)$ pip install httpx |
在服务器运行的情况下,导航到http:// localhost:8000 / async /。 应该立即看到响应:
1 |
Non-blocking HTTP request |
在终端可以看到如下:
1 2 3 4 5 6 7 |
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 /。 得到响应大约需要五秒钟:
1 |
Blocking HTTP request |
终端显示:
1 2 3 4 5 6 7 |
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创建一个新路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# 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的字符串列表和一个名为flavour的字符串。 这些将分别默认为可吸烟的肉和“甜宝贝雷的”的列表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# 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的变量,只要第一个对象不是“肋骨”即可。 for循环将味道(读取:Sweet Baby Ray’s)异步应用于可抽烟(读取:烟熏肉)。
不要忘记导入:
1 |
from typing import List |
List用于额外的键入功能。 这不是必需的,可以轻松省略(只需在“ smokables”参数声明后的:List [str]旁边)。
接下来,创建使用异步烟雾功能的异步视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# 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是用逗号分隔的要吸烟的列表,而味道则是您要对其应用的内容。
该视图要做的第一件事(在标准同步视图中无法完成)是使用asyncio.get_event_loop()抢占事件循环。 然后,它分析查询参数(如果适用)(并对最终的打印语句进行一些字符串清除)。 如果我们不传递任何烟雾,则to_smoke默认为“肉”。 最后,返回响应以使用户知道他们正在准备美味的烧烤餐。
大。 保存文件,然后返回浏览器并导航到http:// localhost:8000 / smoke_some_meats /。 您应该得到以下答复:
1 |
Smoking some meats.... |
在控制端显示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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? |
请注意,在记录200响应之前,肋骨是如何开始吸烟的。 这是工作中的异步性:当烟雾功能最初休眠一秒钟时,视图完成处理并返回响应。 最终用户将在肉类开始吸烟时看到响应。
还值得注意的是,如果您使用Django的dev服务器,则服务器将返回正确的响应,但是异步不会发生。 这是控制台日志的显示方式:
1 2 |
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 =冰淇淋,香蕉,奶酪和风味=金邦士蜜粉。 (空格将方便地自动转换)
浏览器:
1 |
Smoking some ice cream, bananas, and cheese.... |
终端:
1 2 3 4 5 6 7 8 9 10 11 |
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? |
烧肉
同步通话
问:如果在异步视图中进行同步调用怎么办?
如果从非异步视图调用非异步函数,将会发生相同的事情。
–
为了说明这一点,请在您的views.py中创建一个新的辅助函数,称为oversmoke:
1 2 3 4 5 6 |
# 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?") |
非常简单:我们只是同步等待五秒钟。
创建调用此函数的视图:
1 2 3 4 5 |
# hello_async/views.py async def burn_some_meats(request): oversmoke() return HttpResponse(f"Burned some meats.") |
最后,在项目的URLconf中连接路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 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:
1 |
Burned some meats. |
请注意,如何花五秒钟才能最终从浏览器获得响应。 您还应该同时收到控制台输出:
1 2 |
Who doesn't love burnt meats? INFO: 127.0.0.1:40682 - "GET /burn_some_meats HTTP/1.1" 200 OK |
可能值得注意的是,无论您使用的是WSGI还是基于ASGI的服务器,都会发生相同的事情。
同步和异步通话
问:如果在异步视图中进行同步和异步调用怎么办?
不要这样
同步视图和异步视图往往可以最佳地用于不同的目的。 如果您在异步视图中具有阻止功能,那么充其量仅比使用同步视图更好。
同步到异步
如果您需要在异步视图内进行同步调用(例如,通过Django ORM与数据库进行交互),请使用sync_to_async作为包装器或装饰器。
例:
1 2 3 4 5 6 7 |
# 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)") |
在头部导入:
1 |
from asgiref.sync import sync_to_async |
加到urls.py中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# 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/.
显示:
1 2 3 4 5 6 7 |
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,并且想做一些简单的事情(例如向新订户发送电子邮件或调用外部API),那么异步视图是一种快速轻松实现此目标的好方法。如果您需要执行大量,长时间运行的后台进程,则仍然需要使用Celery或RQ。
应该注意的是,为了有效地使用异步视图,您应该仅在视图中进行异步调用。另一方面,任务队列在单独的进程上使用工作程序,因此能够在多个服务器的后台运行同步调用。
顺便说一句,您绝对不必在异步视图和消息队列之间进行选择-您可以轻松地串联使用它们。例如:您可以使用异步视图发送电子邮件或对数据库进行一次性修改,但是Celery每晚在计划的时间清理数据库或生成并发送客户报告。
何时使用
对于未开发项目,请尽可能利用异步视图并以异步方式编写I / O流程。就是说,如果大多数视图仅需要调用数据库并在返回数据之前进行一些基本处理,那么与坚持同步视图相比,您不会看到太多的增长(如果有的话)。
对于棕地项目,如果您几乎没有I / O进程,则坚持同步视图。如果确实有许多I / O进程,请以异步方式重写它们非常容易。将同步I / O重写为异步并不容易,因此您可能要在尝试重写为异步之前优化同步I / O和视图。另外,将同步过程与异步视图混合在一起绝不是一个好主意。
在生产中,请确保使用Gunicorn来管理Uvicorn,以便同时利用(通过Uvicorn)和并行性(通过Gunicorn工人):
1 |
gunicorn -w 3 -k uvicorn.workers.UvicornWorker hello_async.asgi:application |
结论
总之,尽管这是一个简单的用例,但它应该使您大致了解Django的新异步视图打开的可能性。 您可以在异步视图中尝试其他一些操作,例如发送电子邮件,调用第三方API以及写入文件。 考虑一下代码中具有简单过程的视图,这些视图不一定需要直接向最终用户返回任何内容,而是可以将这些视图快速转换为异步视图。
有关Django新发现的异步性的更多信息,请参见这篇出色的文章,内容涉及同一主题以及多线程和测试。
原文:https://testdriven.io/blog/django-async-views/