почему моя сопрограмма блокирует весь экземпляр торнадо?

from tornado import web, gen
import tornado, time

class CoroutineFactorialHandler(web.RequestHandler):
    @web.asynchronous
    @gen.coroutine
    def get(self, n, *args, **kwargs):
        n = int(n)
        def callbacker(iterator, callback):
            try:
                value = next(iterator)
            except StopIteration:
                value = StopIteration
            callback(value)

        def factorial(n):
            x = 1
            for i in range(1, n+1):
                x *= i
                yield

            yield x

        iterator = factorial(n)
        t = time.time()
        self.set_header("Content-Type", "text/plain")
        while True:
            response = yield gen.Task(callbacker, iterator)
            #log.debug("response: %r" %response)
            if response is StopIteration:
                break
            elif response:
                self.write("took : %f sec" %(time.time() - t))
                self.write("\n")
                self.write("f(%d) = %d" %(n, response))

        self.finish()

application = tornado.web.Application([
    (r"^/coroutine/factorial/(?P<n>\d+)", CoroutineFactorialHandler),
    #http://localhost:8888/coroutine/factorial/<int:n>
])

if __name__ == "__main__":
    application.listen(8888)
    ioloop = tornado.ioloop.IOLoop.instance()
    ioloop.start()

21 строка, выдернутая выше, — это простой калькулятор факториала. он зацикливается N раз, как генератор.

проблема в том, что когда этот код выполняется, он блокирует весь торнадо.

чего я хочу добиться, так это написать помощника для торнадо, который обрабатывает генераторы как сопрограмму и, следовательно, может обслуживать запросы асинхронно. (Я прочитал Использование простого генератора Python в качестве сопрограммы в асинхронном обработчике Tornado?)

почему простой цикл увеличения и умножения на n блокирует весь торнадо?

редактировать: я отредактировал код, чтобы включить все приложение, которое вы можете запустить и протестировать. Я запускаю торнадо 3.1.1 на питоне 2.7.


person thkang    schedule 20.09.2013    source источник
comment
Может ли ваш get принять такие аргументы? (Когда я пробую это с Tornado 3.1.1 на Python 2.7.2, я получаю TypeError: get() takes at least 2 arguments (1 given). Я не думаю, что это ваша проблема — если я изменю его, чтобы не принимать аргументы и использовать self.get_argument(n), я думаю, что это демонстрирует вашу проблему в любом случае. Но я не уверен. Итак, это действительно ваш код? Если да, то какую версию вы используете?   -  person abarnert    schedule 20.09.2013
comment
@abarnert Я отредактировал код. Если вы все еще заинтересованы, пожалуйста, посмотрите.   -  person thkang    schedule 21.09.2013
comment
А, я вижу, вы хотели использовать компонент пути, а не строку запроса. Имеет смысл. Во всяком случае, я не думал, что это ваша проблема — как я уже сказал, моя отредактированная версия, использующая self.get_argument для чтения строки запроса, демонстрирует такое же поведение. У меня нет ответа для вас. Я посмотрю повнимательнее, когда у меня будет шанс, но, надеюсь, кто-то еще, кто использует Tornado больше, чем я, придет первым.   -  person abarnert    schedule 21.09.2013


Ответы (1)


Вы должны помнить, что Tornado работает в одном потоке. Код разбит на задачи, которые последовательно вызываются в основном цикле. Если выполнение одной из этих задач занимает много времени (из-за блокирующих функций, таких как time.sleep(), или некоторых сложных вычислений, таких как факториал), в результате будет заблокирован весь цикл.

Итак, что вы можете сделать...? Одним из решений является создание цикла с использованием IOLoop.add_callback():

from tornado import web, gen
import tornado, time

class CoroutineFactorialHandler(web.RequestHandler):
    def factorial(self, limit=1):
        count = 1
        fact = 1
        while count <= limit:
            yield fact
            count = count + 1
            fact = fact * count 

    def loop(self):
        try:
            self.fact = self.generator.next()
            tornado.ioloop.IOLoop.instance().add_callback(self.loop)
        except StopIteration:
            self.write("took : %f sec" %(time.time() - self.t))
            self.write("\n")
            self.write("f(%d) = %d" % (self.n, self.fact))
            self.finish()

    @web.asynchronous
    def get(self, n, *args, **kwargs):
        self.n = int(n)
        self.generator = self.factorial(self.n)
        self.t = time.time()
        self.set_header("Content-Type", "text/plain")
        tornado.ioloop.IOLoop.instance().add_callback(self.loop)

application = tornado.web.Application([
    (r"^/coroutine/factorial/(?P<n>\d+)", CoroutineFactorialHandler),
    #http://localhost:8888/coroutine/factorial/<int:n>
])

if __name__ == "__main__":
    application.listen(8888)
    ioloop = tornado.ioloop.IOLoop.instance()
    ioloop.start()

Каждое умножение здесь является отдельной задачей, что позволяет смешивать factorial вызовы генератора из разных запросов. Это хороший подход, если каждый вызов генератора занимает одинаковое количество времени. Однако, если вы будете вычислять 100000! затем в какой-то момент времени последовательность задач будет выглядеть как 90000!*90001, 90001!*90002 и так далее. Это занимает некоторое время, чтобы вычислить это, даже если это только одно умножение вместо всего цикла, поэтому другой запрос будет отложен. Для такого большого входного целого числа вы должны выполнять вычисления в другом потоке, чтобы получить справедливую долю процессорного времени для запроса. Вот пример, как это сделать: http://lbolla.info/blog/2013/01/22/blocking-tornado

В качестве примечания, в факториале у вас много избыточности, поэтому вы должны хранить список решений для некоторого n в памяти, чтобы мгновенно возвращать их, не тратя время процессора на одни и те же вычисления снова и снова.

person Nykakin    schedule 21.09.2013
comment
Привет, я проверил блог, указанный в этом ответе, он использует пул потоков с заданным МАКСИМАЛЬНЫМ КОЛИЧЕСТВОМ ПОТОКОВ. Как настроить максимальное количество потоков? И если я собираюсь применить эту логику для каждого действия моего веб-портала, я в конечном итоге создам множество потоков ... как правило, то, что делает большинство веб-серверов! - person Shafiul; 23.01.2014
comment
Помните, что вам следует избегать создания тем. Весь смысл асинхронного программирования заключается в использовании одного потока и чередовании вызовов. Вы должны попытаться переписать свой код, чтобы сделать его асинхронным, использовать вызовы, которые вычисляются быстро, использовать асинхронную библиотеку (Tornado может использовать библиотеки Twisted) или перенести часть работы на клиент с помощью JavaScript (код в ответе выше является просто примером, поскольку клиент Браузер с этим справляется). Следует избегать создания потоков, поскольку это возвращает вам все проблемы с потоками, которых Tornado позволяет избежать, например, в вашем вопросе. - person Nykakin; 23.01.2014