常见问题解答

为什么这个包含 time.sleep() 的例子不能并行运行?

许多人第一次尝试 Tornado 的并发会是这样的

class BadExampleHandler(RequestHandler):
    def get(self):
        for i in range(5):
            print(i)
            time.sleep(1)

同时获取此处理程序两次,你会发现第二个五秒倒计时要等到第一个完全完成才会开始。原因是 time.sleep 是一个**阻塞**函数:它不允许控制权返回到 IOLoop,以便运行其他处理程序。

当然,time.sleep 在这些例子中只是一个占位符,重点是展示当处理程序中的某些内容变慢时会发生什么。无论实际代码在做什么,为了实现并发,必须用非阻塞等效项替换阻塞代码。这意味着以下三种情况之一

  1. 找到一个协程友好的等效项。 对于 time.sleep,使用 tornado.gen.sleep(或 asyncio.sleep

    class CoroutineSleepHandler(RequestHandler):
        async def get(self):
            for i in range(5):
                print(i)
                await gen.sleep(1)
    

    当这个选项可用时,它通常是最好的方法。参见 Tornado wiki,了解可能对您有用的异步库的链接。

  2. 找到一个基于回调的等效项。 与第一个选项类似,许多任务都提供基于回调的库,尽管它们的使用比专为协程设计的库要复杂一些。将基于回调的函数改造成一个 future

    class CoroutineTimeoutHandler(RequestHandler):
        async def get(self):
            io_loop = IOLoop.current()
            for i in range(5):
                print(i)
                f = tornado.concurrent.Future()
                do_something_with_callback(f.set_result)
                result = await f
    

    同样,Tornado wiki 可以帮助您找到合适的库。

  3. 在另一个线程上运行阻塞代码。 当异步库不可用时,concurrent.futures.ThreadPoolExecutor 可用于在另一个线程上运行任何阻塞代码。这是一个通用的解决方案,可用于任何阻塞函数,无论是否存在异步对应项。

    class ThreadPoolHandler(RequestHandler):
        async def get(self):
            for i in range(5):
                print(i)
                await IOLoop.current().run_in_executor(None, time.sleep, 1)
    

参见 Tornado 用户指南中的 异步 I/O 章节,了解有关阻塞和异步函数的更多信息。

我的代码是异步的。为什么它在两个浏览器选项卡中不能并行运行?

即使处理程序是异步的且非阻塞的,验证这一点也可能出乎意料地棘手。浏览器会识别出您试图在两个不同的选项卡中加载同一个页面,并延迟第二个请求,直到第一个完成。为了绕过这个问题并查看服务器实际上是否在并行工作,可以执行以下两项操作之一

  • 在您的 URL 中添加一些内容以使其唯一。在两个选项卡中不使用 https://127.0.0.1:8888,而是在一个选项卡中加载 https://127.0.0.1:8888/?x=1,在另一个选项卡中加载 https://127.0.0.1:8888/?x=2

  • 使用两个不同的浏览器。例如,Firefox 能够加载 URL,即使 Chrome 选项卡中正在加载同一个 URL。