защо моята съпрограма блокира целия екземпляр на торнадо?

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 на python 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 извиквания на генератор от различни заявки. Това е добър подход, ако всяко обаждане до генератор отнема еднакво време. Но ако ще изчислите 100 000! тогава в даден момент задачите в последователност ще изглеждат като 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