web-dev-qa-db-ja.com

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

このコードは、同期的に実行され、リクエストが終了するまで待機することを除いて、機能することが期待されます。

現在のリクエストをすぐに返しながら、このタスクを非同期的に呼び出すright方法は何ですか?

16
Yuval Adam

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

このようなものは、おそらく将来的に竜巻自体に追加されるでしょう。

14
Ben Darnell

ポストリクエストで時間のかかるタスクがあり、おそらく30分以上必要ですが、クライアントはすぐに結果を返す必要があります。

まず、 IOLoop.current()。spawn_callback を使用しました。できます!だが!最初のリクエストタスクが実行されている場合、2番目のリクエストタスクはブロックされています! spawn_callbackを使用すると、すべてのタスクがメインイベントループにあるため、1つのタスクが同期実行され、他のタスクはブロックされます。

最後に、 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:80 にアクセスすると、正常に実行されていることがわかります。

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

みんなを助けたい!

8
Legolas Bloom

toro の使用をお勧めします。これは、タスクのバックグラウンドキューを設定するための比較的単純なメカニズムを提供します。

次のコード(たとえば、queue.pyに配置)は、キューに何かが入るまで単純に待機する単純な「worker()」を開始します。 queue.add(function,async,*args,**kwargs)を呼び出すと、キューにアイテムが追加され、worker()がウェイクアップして、タスクが開始されます。

@ gen.coroutineでラップされたバックグラウンドタスクとラップされていないバックグラウンドタスクをサポートできるように、asyncパラメーターを追加しました。

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)
7
Mike N

これが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}')
0
valem