Tornado中的长轮询是如何工作的?

4
在 Tornado 的 聊天室示例 中,有一个类似这样的方法:
@tornado.web.asynchronous
def post(self):
    cursor = self.get_argument("cursor", None)
    global_message_buffer.wait_for_messages(self.on_new_messages,
                                            cursor=cursor)

我对这个长轮询的东西还比较陌生,而且我不太明白线程的工作原理,尽管它说:

通过使用非阻塞网络I/O,Tornado可以扩展到成千上万个打开的连接......

我的理论是通过制作一个简单的应用程序:

import tornado.ioloop
import tornado.web
import time

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        print("Start request")
        time.sleep(4)
        print("Okay done now")
        self.write("Howdy howdy howdy")
        self.finish()

application =  tornado.web.Application([
    (r'/', MainHandler),
])

如果我连续发送两个请求(比如我打开了两个浏览器窗口并快速刷新),我会看到这个:

Start request
Start request
Okay done now
Okay done now

相反,我看到
Start request
Okay done now
Start request
Okay done now

这让我相信,在这种情况下它确实是阻塞的。我的代码为什么会阻塞,我该如何使代码达到预期效果?我在Windows 7上使用core i7和linux Mint 13盒子上获得了相同的输出,其中我认为有两个核心。
编辑:
我找到了一种方法 - 如果有人能提供一个跨平台的方法(我不太担心性能,只要它是非阻塞的),我会接受那个答案。

3
time.sleep 阻塞了 Tornado IOLoop,从而停止了所有处理。你根本不需要多个线程(尽管在生产中可能需要),只需不要睡眠即可。相反,向 IOLoop 添加超时:http://www.tornadoweb.org/en/stable/ioloop.html#tornado.ioloop.IOLoop.add_timeout - Cole Maclean
请从Dano和John提供的答案中选择正确的答案。 - Phyo Arkar Lwin
3个回答

6
原问题的代码问题在于当你调用time.sleep(4)时,你实际上会阻塞事件循环4秒钟。而且被接受的答案也没有解决这个问题(在我看来)。
Tornado中的异步服务是建立在信任基础上的。Tornado会在发生某些事情时调用你的函数,但它相信你会尽快将控制权归还给它。如果你使用time.sleep()进行阻塞,那么这种信任就被破坏了——Tornado无法处理新连接。
使用多个线程只是隐藏了错误;运行具有数千个线程的Tornado(以便可以同时提供1000个连接)将非常低效。适当的方式是运行一个仅在Tornado内部阻塞的单个线程(在select或Tornado用于监听事件的其他方式上)——而不是在你的代码上(确切地说:永远不要在你的代码上阻塞)。
正确的解决方案是在get(self)之前立即返回(而不调用self.finish()),像这样:
class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        print("Starting")

当然,您必须记住此请求仍然是打开的,并且稍后可以调用write()finish()

我建议您查看聊天演示。一旦去掉身份验证,您将得到一个非常不错的异步长轮询服务器示例。


我确实看了一下聊天演示,其中包含的“现实生活”代码比我要找的要多得多 - 即使将其缩减。我意识到(现在)sleep并不是让它工作的正确方法... - Wayne Werner
不幸的是,执行此操作仍会产生“开始”并阻塞,直到第一个请求完全完成(至少在我看来是这样)。 - Wayne Werner
我同意聊天演示有点臃肿。 :) 我所做的就是移除了所有身份验证相关的内容,这样只剩下一个非常小巧的应用程序,可以清晰地展示这些概念(我还重命名了一些函数-wait_for_messages 重命名为 add_to_waiters 等)。关于第二个评论:不,它不会,这就是它的美妙之处。它可以接受新的连接,同时保持旧的连接打开状态(直到其他事件-例如另一个请求-关闭它们)。Twisted教程的第一部分很好地解释了异步模式。 - johndodo

6
将测试应用程序转换为不会阻塞IOLoop的形式的正确方法如下:
from tornado.ioloop import IOLoop
import tornado.web
from tornado import gen
import time

@gen.coroutine
def async_sleep(timeout):
    """ Sleep without blocking the IOLoop. """
    yield gen.Task(IOLoop.instance().add_timeout, time.time() + timeout)

class MainHandler(tornado.web.RequestHandler):
    @gen.coroutine
    def get(self):
        print("Start request")
        yield async_sleep(4)
        print("Okay done now")
        self.write("Howdy howdy howdy")
        self.finish()

if __name__ == "__main__":
    application =  tornado.web.Application([
        (r'/', MainHandler),
    ])
    application.listen(8888)
    IOLoop.instance().start()

区别在于用一个不会阻塞IOLoop的方法代替对time.sleep的调用。Tornado旨在处理大量并发I/O,而不需要多个线程/子进程,但如果您使用同步API,它仍然会被阻塞。为了使您的长轮询解决方案以您想要的方式处理并发性,您必须确保没有长时间运行的调用会被阻塞。


1
自 Tornado 5.0 版本开始,asyncio 已经被自动启用,所以只需将 time.sleep(4) 更改为 await asyncio.sleep(4),并将 @tornado.web.asynchronous def get(self): 更改为 async def get(self): 即可解决问题。
import tornado.ioloop
import tornado.web
import asyncio

class MainHandler(tornado.web.RequestHandler):
    async def get(self):
        print("Start request")
        await asyncio.sleep(4)
        print("Okay done now")
        self.write("Howdy howdy howdy")
        self.finish()

app =  tornado.web.Application([
    (r'/', MainHandler),
])
app.listen(8888)
tornado.ioloop.IOLoop.current().start()

输出:

Start request
Start request
Okay done now
Okay done now

来源:


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