Tornado的'@run_on_executor'是阻塞的

7
我想问一下tornado.concurrent.run_on_executor(后面简称run_on_executor)的工作原理,因为我可能不理解如何运行同步任务以避免阻塞主IOLoop。
我找到的所有使用run_on_executor的示例都只是使用time来阻塞循环。使用time模块时效果很好,但是当我尝试使用run_on_executor进行一些耗时的计算时,任务会阻塞IOLoop。虽然我可以看到应用程序使用了多个线程,但仍然会阻塞。
我想使用run_on_executor来哈希密码,使用bcrypt,但我用这个计算替换了它,以获得一些额外的测试时间。
这里有一个小应用程序,以演示我的困惑。
from tornado.options import define, options
import tornado.web
import tornado.httpserver
from tornado import gen
from tornado.concurrent import run_on_executor
import tornado.httpclient
import tornado.escape
import time
import concurrent.futures
import urllib


executor = concurrent.futures.ThreadPoolExecutor(20)
define("port", default=8888, help="run on the given port", type=int)


# Should not be blocking ?
class ExpHandler(tornado.web.RequestHandler):
    _thread_pool = executor

    @gen.coroutine
    def get(self, num):
        i = int(num)
        result = yield self.exp(2, i)
        self.write(str(result))
        self.finish()

    @run_on_executor(executor="_thread_pool")
    def exp(self, x, y):
        result = x ** y
        return(result)


class NonblockingHandler(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self):
        http_client = tornado.httpclient.AsyncHTTPClient()
        try:
            response = yield http_client.fetch("http://www.google.com/")
            self.write(response.body)
        except tornado.httpclient.HTTPError as e:
            self.write(("Error: " + str(e)))
        finally:
            http_client.close()
        self.finish()


class SleepHandler(tornado.web.RequestHandler):
    _thread_pool = executor

    @gen.coroutine
    def get(self, sec):
        sec = float(sec)
        start = time.time()
        res = yield self.sleep(sec)
        self.write("Sleeped for {} s".format((time.time() - start)))
        self.finish()

    @run_on_executor(executor="_thread_pool")
    def sleep(self, sec):
        time.sleep(sec)
        return(sec)


class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r'/exp/(?P<num>[^\/]+)?', ExpHandler),
            (r'/nonblocking/?', NonblockingHandler),
            (r'/sleep/(?P<sec>[^\/]+)?',SleepHandler)
        ]
        settings = dict(
            debug=True,
            logging="debug"
        )
        tornado.web.Application.__init__(self, handlers, **settings)


def  main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    io_loop = tornado.ioloop.IOLoop.instance()
    io_loop.start()


if __name__ == "__main__":
    main()

如果您能解释为什么在 executor 中运行的 ExpHandler 会阻塞循环,我将非常感激。

1个回答

7

Python(至少在CPython实现中)有一个全局解释器锁(GIL),阻止多个线程同时执行Python代码。特别地,任何在单个Python操作码中运行的内容都是不可中断的,除非它调用明确释放GIL的C函数。一个大的指数计算使用**将一直持有GIL,并因此阻塞所有其他Python线程,而对bcrypt()的调用将释放GIL,使其他线程可以继续工作。


1
我已经了解了GIL,但显然没有足够理解这个概念,但现在我看得更清楚了。 我也没有意识到bcrypt是用'C'实现的,具有GIL释放功能。 所以下次如果我需要实现一些计算密集型任务,我会使用专为此类目的设计的库。 我还尝试通过简单的乘法生成函数来实现**(pow),它不是非常高效,但至少不会阻塞。非常好(: 非常感谢您的澄清。 - Maros Mitucha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接