在Tornado中运行异步后台任务

时间:2014-02-27 22:39:18

标签: python asynchronous tornado

阅读Tornado文档,非常清楚如何调用异步函数来返回响应:

class GenAsyncHandler(RequestHandler):
    @gen.coroutine
    def get(self):
        http_client = AsyncHTTPClient()
        response = yield http_client.fetch("http://example.com")
        do_something_with_response(response)
        self.render("template.html")

缺少的是如何异步调用与当前请求无关的后台任务:

class GenAsyncHandler(RequestHandler):
    @gen.coroutine
    def _background_task():
        pass  # do lots of background stuff

    @gen.coroutine
    def get(self):
        _dont_care = yield self._background_task()
        self.render("template.html")

此代码可以使用,除非它同步运行并且请求等待它直到它完成。

在立即返回当前请求的同时,异步调用此任务的正确方法是什么?

5 个答案:

答案 0 :(得分:13)

更新:自Tornado 4.0(2014年7月)以来,IOLoop.spawn_callback方法提供了以下功能。

不幸的是,这有点棘手。您需要同时从当前请求中分离后台任务(以便后台任务中的失败不会导致请求中出现随机异常)并确保某些内容正在侦听后台任务的结果(如果没有别的话,记录它的错误)。这意味着:

from tornado.ioloop import IOLoop
from tornado.stack_context import run_in_stack_context, NullContext
IOLoop.current().add_future(run_in_stack_context(NullContext(), self._background_task),
                            lambda f: f.result())

这样的事情将来可能会被添加到龙卷风中。

答案 1 :(得分:7)

我建议使用toro。它提供了一种相对简单的机制来设置任务的后台队列。

以下代码(例如,放在queue.py中)启动一个简单的" worker()"只是等待他的队列中有东西。如果你打电话给queue.add(function,async,*args,**kwargs),这会在队列中添加一个项目,该队列将唤醒worker(),然后启动任务。

我添加了async参数,这样就可以支持包含在@ gen.coroutine中的后台任务和那些没有的后台任务。

import toro,tornado.gen
queue = toro.Queue()
@tornado.gen.coroutine
def add(function,async,*args,**kwargs):
   item = dict(function=function,async=async,args=args,kwargs=kwargs)
   yield queue.put(item)

@tornado.gen.coroutine
def worker():
   while True:
      print("worker() sleeping until I get next item")
      item = yield queue.get()
      print("worker() waking up to process: %s" % item)
      try:
         if item['async']:
            yield item['function'](*item['args'],**item['kwargs'])
         else:
            item['function'](*item['args'],**item['kwargs'])
      except Exception as e:
         print("worker() failed to run item: %s, received exception:\n%s" % (item,e))

@tornado.gen.coroutine
def start():
   yield worker()

在您的主龙卷风应用中:

import queue
queue.start()

现在你可以非常简单地安排一个背景任务:

def my_func(arg1,somekwarg=None):
   print("in my_func() with %s %s" % (arg1,somekwarg))

queue.add(my_func,False,somearg,somekwarg=someval)

答案 2 :(得分:7)

我在发布请求中有一项耗时的任务,可能需要超过30分钟,但客户需要立即返回结果。

首先,我使用IOLoop.current().spawn_callback。有用!但!如果第一个请求任务正在运行,则第二个请求任务被阻止!因为当使用spawn_callback时所有任务都在主事件循环中,所以一个任务是同步执行,其他任务被阻止。

最后,我使用tornado.concurrent。例如:

bundle

并访问http://127.0.0.1:8000,您可以看到它运行正常:

import datetime
import time

from tornado.ioloop import IOLoop
import tornado.web
from tornado import concurrent

executor = concurrent.futures.ThreadPoolExecutor(8)


class Handler(tornado.web.RequestHandler):

    def get(self):
        def task(arg):
            for i in range(10):
                time.sleep(1)
                print(arg, i)

        executor.submit(task, datetime.datetime.now())
        self.write('request accepted')


def make_app():
    return tornado.web.Application([
        (r"/", Handler),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8000, '0.0.0.0')
    IOLoop.current().start()

想要帮助大家!

答案 3 :(得分:2)

简单地说:

self._background_task()

_background_task协程返回一个Future,在协程完成之前一直未解析。如果您产生Future,而只是立即执行下一行,则get()不会等待_background_task完成。

一个有趣的细节是,在_background_task完成之前,它会保留对self的引用。 (顺便说一句,不要忘记添加self作为参数。)在_background_task完成之前,您的RequestHandler不会被垃圾收集。

答案 4 :(得分:0)

这是我在2019年的答案!

从一些慢速的非阻塞代码开始。参见:http://www.tornadoweb.org/en/stable/faq.html#id2

async def _do_slow_task(self, pk):
    await asyncio.sleep(pk)
    logger.info(f'Finished slow task after {pk} seconds')

请参见此处以了解异步与阻止之间的区别:http://www.tornadoweb.org/en/stable/guide/async.html#asynchronous-and-non-blocking-i-o

然后,将您的请求方法设为具有async/await语法的协程,使其无阻塞,以便并行处理多个请求。< / p>

async def post(self):
    """Make a few requests with different pks and you should see that
        the numbers logged are in ascending order.
    """
    pk = self.get_query_argument('pk')
    try:
        record = await self.db_queryone(
            f"SELECT * FROM records WHERE id = {int(pk)};"
        )
    except Exception as e:
        self.set_status(400)
        self.write(str(e))
        return
    await self._do_slow_task(pk)
    self.write(f'Received {pk}')

现在,稍微修改一下方法,使其在后台运行

  

无需等待结果即可“解雇”协程

,以便客户端立即收到响应。参见:http://www.tornadoweb.org/en/stable/guide/coroutines.html#how-to-call-a-coroutine

async def post(self):
    """Make a few requests with different pks and you should see responses
        right away, and eventually log messages with the numbers in
        ascending order.
    """
    pk = self.get_query_argument('pk')
    try:
        record = await self.db_queryone(
            f"SELECT * FROM records WHERE id = {int(pk)};"
        )
    except Exception as e:
        self.set_status(400)
        self.write(str(e))
        return
    IOLoop.current().spawn_callback(self._do_slow_task, pk)
    self.write(f'Received {pk}')