【问题标题】:Running an async background task in Tornado在 Tornado 中运行异步后台任务
【发布时间】:2014-03-31 16:30:49
【问题描述】:

阅读 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")

这段代码应该可以工作,除了它同步运行并且请求在它上面等待直到它完成。

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

【问题讨论】:

    标签: python asynchronous tornado


    【解决方案1】:

    这是我在 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 语法将您的请求方法设为协程,使其非阻塞,以便并行处理多个请求。

    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}')
    

    【讨论】:

      【解决方案2】:

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

      不幸的是,这有点棘手。您需要从当前请求中分离后台任务(以便后台任务中的失败不会导致向请求中抛出随机异常)并确保 something 正在监听后台任务的结果(如果没有别的,记录它的错误)。这意味着这样的事情:

      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())
      

      这样的东西将来可能会被添加到龙卷风本身。

      【讨论】:

      • 希望最终能看到这一点。感谢您的详尽回答,以及您在龙卷风方面的工作:)
      【解决方案3】:

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

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

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

      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()
      

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

      2017-01-17 22:42:10.983632 0
      2017-01-17 22:42:10.983632 1
      2017-01-17 22:42:10.983632 2
      2017-01-17 22:42:13.710145 0
      2017-01-17 22:42:10.983632 3
      2017-01-17 22:42:13.710145 1
      2017-01-17 22:42:10.983632 4
      2017-01-17 22:42:13.710145 2
      2017-01-17 22:42:10.983632 5
      2017-01-17 22:42:16.694966 0
      2017-01-17 22:42:13.710145 3
      2017-01-17 22:42:10.983632 6
      2017-01-17 22:42:16.694966 1
      2017-01-17 22:42:13.710145 4
      2017-01-17 22:42:10.983632 7
      2017-01-17 22:42:16.694966 2
      2017-01-17 22:42:13.710145 5
      2017-01-17 22:42:10.983632 8
      2017-01-17 22:42:16.694966 3
      2017-01-17 22:42:13.710145 6
      2017-01-17 22:42:19.790646 0
      2017-01-17 22:42:10.983632 9
      2017-01-17 22:42:16.694966 4
      2017-01-17 22:42:13.710145 7
      2017-01-17 22:42:19.790646 1
      2017-01-17 22:42:16.694966 5
      2017-01-17 22:42:13.710145 8
      2017-01-17 22:42:19.790646 2
      2017-01-17 22:42:16.694966 6
      2017-01-17 22:42:13.710145 9
      2017-01-17 22:42:19.790646 3
      2017-01-17 22:42:16.694966 7
      2017-01-17 22:42:19.790646 4
      2017-01-17 22:42:16.694966 8
      2017-01-17 22:42:19.790646 5
      2017-01-17 22:42:16.694966 9
      2017-01-17 22:42:19.790646 6
      2017-01-17 22:42:19.790646 7
      2017-01-17 22:42:19.790646 8
      2017-01-17 22:42:19.790646 9
      

      想帮助大家!

      【讨论】:

      • 您无法同时运行的原因是因为time.sleep 是阻塞的,正如我在这里发现的那样:tornadoweb.org/en/stable/faq.html#id2 但是如果您使用非阻塞功能,则不需要龙卷风。并发和spawn_callback 应该足够了
      【解决方案4】:

      我建议使用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)
      

      【讨论】:

        【解决方案5】:

        简单地做:

        self._background_task()
        

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

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

        【讨论】:

        • 事实证明这比我想象的要困难一些,我的任务是做一些不容易被“异步”(posix socket IO)的不平凡的事情。由于它目前阻塞了整个 IO 循环,因此很难测试,但这似乎是正确的方向,谢谢!
        • 但是如果你不让/等待Future,那么方法体真的会运行吗?
        猜你喜欢
        • 2016-03-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-10-18
        • 1970-01-01
        • 2022-09-25
        • 1970-01-01
        相关资源
        最近更新 更多