Как понимать асинхронность в Tornado?

Нашёл где-то в дебрях интернетов пример запуска асихронного сервера на Tornado с корутинами. Код:
#!/usr/bin/python
# -*- coding: utf-8 -*-

import logging
import time

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.gen
from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class MainHandler(tornado.web.RequestHandler):

    @tornado.gen.coroutine
    def outer(self):
        logging.info('outer starts')
        yield self.inner()
        logging.info("some text here")
        yield self.inner()
        logging.info('outer ends')
        return 'hello'

    @tornado.gen.coroutine
    def inner(self):
        time.sleep(2)
        logging.info('inner runs')

    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def get(self):
        res = yield self.outer()
        self.write(res)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    app = tornado.web.Application(handlers=[(r"/", MainHandler)])
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()


Судя по документации Tornado и куче прочитанных статей в интернетах, декоратор "@tornado.gen.coroutine" в купе с yield внутри функции, даёт асинхронное выполнение задач. Однако, запуск вышевставленного кода меня огорчил :-( Я ожидал, что лог из метода outer() ("some text here") напечатается раньше, чем из метода inner() (специльно таймаут вставил). И весь аутпут в лог выполнился очень даже синхронно. Более того, я убрал из кода все эти декораторы от Tornado, убрал все yield'ы, то есть сделал код чисто сихронным. Вуаля - результат от асинхронного кода не отличается: тесты проводил на двух вкладках браузера, запускал оновременный Reload и наблюдал за происходящим в консоли запущенного сервера. Мало того, что задачи для одного клиента выполнялись синхронней некуда, так ещё и клиенты блокировали друг друга (точнее выстраивались в очередь: сервер отправлял ответ одному клиенту и брался за следующего).

Почему я так думаю относительно ассинхронности: с этой магией хорошо знаком только в разрезе работы с ajax-запросами в JavaScript - запустил запрос, навесил на результат коллбэк, процесс работает и пыхтит дальше, коллбэк отработает по приходу результата от ajax-запроса. Мне казалось, что в Tornado будет так же красиво :-) Ткните меня носом в то место, где я неверно интерпретирую асинхронность в этом замечательном фрэймворке и помогите постигнуть дзен :-) буду очень благодарен!

ЗЫ: с корутинами пока на ВЫ общаюсь, не судите строго :-)
  • Вопрос задан
  • 9421 просмотр
Решения вопроса 1
@ykalchevskiy
tornado.gen.coroutine + yield != асинхронность. Вызовы, которые делаются должны уметь работать асинхронно. Смотрим здесь первый пример:
class AsyncHandler(RequestHandler):
    @asynchronous
    def get(self):
        http_client = AsyncHTTPClient()
        http_client.fetch("http://example.com",
                          callback=self.on_fetch)

    def on_fetch(self, response):
        do_something_with_response(response)
        self.render("template.html")

Метод fetch объекта класса AsyncHTTPClient -- асинхронный. На это указывает аргумент callback (и название класса, конечно :)). Когда страница будет получена, вызовется _on_fetch. Как и в AJAX.
Пример ниже на этой же странице -- это переписанная версия того же самого, просто красивее, без лапши коллбеков. Для этого и нужна пара tornado.gen.coroutine + yield.

Вызов time.sleep(2) блокирует весь ioloop, вместо него можно воспользоваться чем-то типа
yield tornado.gen.Task(tornado.ioloop.IOLoop.current().add_timeout, time.time() + 2)


Но даже заменив эту строку, асинхронности не будет заметна. Это связано с ограничениями браузеров: они не умеют одновременно открывать одну и туже вкладку. Поэтому нужно открыть вкладки в разных браузерах.

Еще одно: декоратор @tornado.web.asynchronous не нужен при использовании @tornado.gen.coroutine.

Вот эту страничку нужно обязательно прочитать.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 3
un1t
@un1t
У вас в коде синхронный вызов time.sleep он блочит процесс.

Асинхронный sleep выглядит примерно так
def delay(self, seconds):
    return gen.Task(ioloop.IOLoop.instance().add_timeout, time.time() + seconds)


Пример использования
@gen.coroutine
def foo(self):
    yield self.delay(5)
Ответ написан
yield для этого и пишется чтобы весь асинхронный код выглядел и работал как синхринный. Это помогает избежать спагетти. Уберите его если нужно чтобы вызовы были асинхронны. Или вызывайте через IOLoop.instance().add_callback(self.inner)
Ответ написан
zhulikovatyi
@zhulikovatyi Автор вопроса
Лог сервера при одновременном запуске двух клиентов:
[I 150218 17:38:41 test:19] outer starts
[I 150218 17:38:43 test:29] inner runs
[I 150218 17:38:43 test:21] some text here
[I 150218 17:38:45 test:29] inner runs
[I 150218 17:38:45 test:23] outer ends
[I 150218 17:38:45 web:1825] 304 GET / (127.0.0.1) 4005.73ms
[I 150218 17:38:45 test:19] outer starts
[I 150218 17:38:47 test:29] inner runs
[I 150218 17:38:47 test:21] some text here
[I 150218 17:38:49 test:29] inner runs
[I 150218 17:38:49 test:23] outer ends
[I 150218 17:38:49 web:1825] 304 GET / (127.0.0.1) 4004.74ms
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы