我只是将旧项目升级到Python 3.6,并发现有这些很酷的新async / await关键字。
我的项目包含一个网络抓取工具,目前效率不高,大约需要7分钟才能完成。 现在,由于我已经使用django restframework来访问我的django应用程序的数据,我认为拥有一个REST端点可以很好,我可以通过一个简单的POST请求从远程启动爬虫。
但是,我不希望客户端同步等待爬虫完成。我只想立即向他发送抓取工具已启动的消息,并在后台启动抓取工具。
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.conf import settings
from mycrawler import tasks
async def update_all_async(deep_crawl=True, season=settings.CURRENT_SEASON, log_to_db=True):
await tasks.update_all(deep_crawl, season, log_to_db)
@api_view(['POST', 'GET'])
def start(request):
"""
Start crawling.
"""
if request.method == 'POST':
print("Crawler: start {}".format(request))
deep = request.data.get('deep', False)
season = request.data.get('season', settings.CURRENT_SEASON)
# this should be called async
update_all_async(season=season, deep_crawl=deep)
return Response({"Success": {"crawl finished"}}, status=status.HTTP_200_OK)
else:
return Response ({"description": "Start the crawler by calling this enpoint via post.", "allowed_parameters": {
"deep": "boolean",
"season": "number"
}}, status.HTTP_200_OK)
我已经阅读了一些教程,以及如何使用循环和内容,但我并没有真正理解......在这种情况下我应该在哪里开始循环?
[编辑] 20/10/2017:
我现在使用线程解决了它,因为它真的是一个“火”并且忘记了#34;任务。但是,我仍然想知道如何使用async / await实现相同的功能。
这是我目前的解决方案:
import threading
@api_view(['POST', 'GET'])
def start(request):
...
t = threading.Thread(target=tasks.update_all, args=(deep, season))
t.start()
...
答案 0 :(得分:6)
这在 Django 3.1+ 中是可能的,在 introducing asynchronous support 之后。
关于异步运行循环,您可以通过使用 uvicorn
或任何其他 ASGI 服务器而不是 gunicorn
或其他 WSGI 服务器运行 Django 来使用它。
不同之处在于,在使用 ASGI 服务器时,已经有一个运行循环,而在使用 WSGI 时需要创建一个。使用 ASGI,您可以直接在 async
或其视图类的继承函数下直接定义 views.py
函数。
假设您使用 ASGI,您有多种方法可以实现这一点,我将描述几种方法(例如,其他选项可以使用 asyncio.Queue
):
start()
异步通过使 start()
异步,您可以直接使用现有的运行循环,而通过使用 asyncio.Task
,您可以触发并忘记到现有的运行循环中。如果你想解雇但记住,你可以创建另一个 Task
来跟进这个,即:
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.conf import settings
from mycrawler import tasks
import asyncio
async def update_all_async(deep_crawl=True, season=settings.CURRENT_SEASON, log_to_db=True):
await tasks.update_all(deep_crawl, season, log_to_db)
async def follow_up_task(task: asyncio.Task):
await asyncio.sleep(5) # Or any other reasonable number, or a finite loop...
if task.done():
print('update_all task completed: {}'.format(task.result()))
else:
print('task not completed after 5 seconds, aborting')
task.cancel()
@api_view(['POST', 'GET'])
async def start(request):
"""
Start crawling.
"""
if request.method == 'POST':
print("Crawler: start {}".format(request))
deep = request.data.get('deep', False)
season = request.data.get('season', settings.CURRENT_SEASON)
# Once the task is created, it will begin running in parallel
loop = asyncio.get_running_loop()
task = loop.create_task(update_all_async(season=season, deep_crawl=deep))
# Fire up a task to track previous down
loop.create_task(follow_up_task(task))
return Response({"Success": {"crawl finished"}}, status=status.HTTP_200_OK)
else:
return Response ({"description": "Start the crawler by calling this enpoint via post.", "allowed_parameters": {
"deep": "boolean",
"season": "number"
}}, status.HTTP_200_OK)
有时您不能只拥有一个 async
函数来将请求路由到第一,as it happens with DRF(截至今天)。
为此,Django 提供了一些有用的 async
adapter functions,但请注意,从同步上下文切换到异步上下文,反之亦然,a small performance penalty 大约需要 1 毫秒。请注意,这一次,运行循环改为收集在 update_all_sync
函数中:
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from django.conf import settings
from mycrawler import tasks
import asyncio
from asgiref.sync import async_to_sync
@async_to_sync
async def update_all_async(deep_crawl=True, season=settings.CURRENT_SEASON, log_to_db=True):
#We can use the running loop here in this use case
loop = asyncio.get_running_loop()
task = loop.create_task(tasks.update_all(deep_crawl, season, log_to_db))
loop.create_task(follow_up_task(task))
async def follow_up_task(task: asyncio.Task):
await asyncio.sleep(5) # Or any other reasonable number, or a finite loop...
if task.done():
print('update_all task completed: {}'.format(task.result()))
else:
print('task not completed after 5 seconds, aborting')
task.cancel()
@api_view(['POST', 'GET'])
def start(request):
"""
Start crawling.
"""
if request.method == 'POST':
print("Crawler: start {}".format(request))
deep = request.data.get('deep', False)
season = request.data.get('season', settings.CURRENT_SEASON)
# Make update all "sync"
sync_update_all_sync = async_to_sync(update_all_async)
sync_update_all_sync(season=season, deep_crawl=deep)
return Response({"Success": {"crawl finished"}}, status=status.HTTP_200_OK)
else:
return Response ({"description": "Start the crawler by calling this enpoint via post.", "allowed_parameters": {
"deep": "boolean",
"season": "number"
}}, status.HTTP_200_OK)
在这两种情况下,函数都会快速返回 200,但从技术上讲,第二个选项更慢。
<块引用>重要事项:在使用 Django 时,这些异步操作中涉及到数据库操作是很常见的。 Django 中的 DB 操作只能是同步的,至少现在是这样,因此您必须在异步上下文中考虑这一点。 sync_to_async()
在这些情况下变得非常方便。
答案 1 :(得分:2)
在我看来,你应该看看celery,这是一个专门为异步任务设计的好工具。它支持Django,当您不希望用户等待服务器上的长时间操作时,它非常有用。在后台运行的每个任务都会收到 task_id ,如果您想要创建另一个服务,可以帮助您,在给定 task_id 的情况下,返回特定任务是否已成功或不,或者到目前为止已完成了多少。