在 Jupyter Notebook 中运行 Tornado 服务器

8
将标准Tornado演示推入后台线程,并将IOLoop推到后台线程中,可以在单个脚本中查询服务器。当Tornado服务器是交互对象(参见Dask或类似工具)时,这很有用。
import asyncio
import requests
import tornado.ioloop
import tornado.web

from concurrent.futures import ThreadPoolExecutor

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

pool = ThreadPoolExecutor(max_workers=2)
loop = tornado.ioloop.IOLoop()

app = make_app()
app.listen(8888)
fut = pool.submit(loop.start)

print(requests.get("https://localhost:8888"))

在标准的Python脚本中,上述内容完全正常(尽管缺少安全关闭)。Jupyter Notebook 是这些交互式Tornado服务器环境的最佳环境。然而,当涉及到Jupyter时,这个想法就会破裂,因为已经有一个活动运行的循环了:

>>> import asyncio
>>> asyncio.get_event_loop()
<_UnixSelectorEventLoop running=True closed=False debug=False>

当在Jupyter notebook中运行上述脚本时,服务器和请求客户端都试图在同一线程中打开连接,代码会挂起。构建新的Asyncio循环和/或Tornado IOLoop似乎没有帮助,我怀疑我在Jupyter本身中缺少某些东西。
问题是:是否可能在Jupyter笔记本内部运行一个活动的Tornado服务器,以便标准python“requests”或类似工具可以从主线程连接到它?如果可能,我希望在用户呈现的代码中尽可能避免使用Asyncio,因为对于初学者来说它相对复杂。
3个回答

3

基于我最近为streamz提交的PR,以下是类似于你的想法并且可行的内容:

class InNotebookServer(object):
    def __init__(self, port):
        self.port = port
        self.loop = get_ioloop()
        self.start()

    def _start_server(self):
        from tornado.web import Application, RequestHandler
        from tornado.httpserver import HTTPServer
        from tornado import gen

        class Handler(RequestHandler):
            source = self

            @gen.coroutine
            def get(self):
                self.write('Hello World')

        application = Application([
            ('/', Handler),
        ])
        self.server = HTTPServer(application)
        self.server.listen(self.port)

    def start(self):
        """Start HTTP server and listen"""
        self.loop.add_callback(self._start_server)


_io_loops = []

def get_ioloop():
    from tornado.ioloop import IOLoop
    import threading
    if not _io_loops:
        loop = IOLoop()
        thread = threading.Thread(target=loop.start)
        thread.daemon = True
        thread.start()
        _io_loops.append(loop)
    return _io_loops[0]

在笔记本中调用

In [2]: server = InNotebookServer(9005)
In [3]: import requests
        requests.get('http://localhost:9005')
Out[3]: <Response [200]>

1

第一部分:让我们嵌套tornado

要找到所需的信息,您必须遵循以下线索,首先查看IPython 7的发行说明中描述的内容。 特别是它将指向文档中有关异步和等待部分的更多信息,并且指向此讨论, 其中建议使用nest_asyncio

关键在于以下内容:

  • A) 或者您欺骗Python运行两个嵌套的事件循环。(nest_asyncio的作用)
  • B) 您可以在已经存在的事件循环上安排协程。(我不确定如何在tornado中实现此操作)

我相信你已经知道这些,但我相信其他读者会欣赏。

不幸的是,除非像在jupyterhub上控制部署并可以将这些行添加到自动加载的IPython启动脚本中,否则无法完全透明地向用户展示。但我认为以下方法足够简单。

import nest_asyncio
nest_asyncio.apply()


# rest of your tornado setup and start code.

第二部分:避免同步代码阻塞事件循环

前一节只是关注能够运行tornado应用程序。但请注意任何同步代码都会阻塞事件循环。因此,当运行print(requests.get("http://localhost:8000"))时,服务器将看起来无法工作,因为您正在阻塞事件循环,等待代码执行完成后才会重新启动事件循环...(理解这一点是留给读者的练习)。您需要从另一个内核中发出print(requests.get("http://localhost:8000")),或者使用aiohttp。

以下是如何以类似于requests的方式使用aiohttp。

import aiohttp
session =  aiohttp.ClientSession()
await session.get('http://localhost:8889')

在这种情况下,由于aiohttp是非阻塞的,事情似乎会正常运行。您可以在此处看到一些额外的IPython魔法,我们可以自动检测异步代码并在当前事件循环上运行它。
一个很酷的练习是在另一个内核中循环运行request.get,并在tornado正在运行的内核中运行sleep(5),然后看到我们停止处理请求...
第三部分:免责声明和其他路线:
这是相当棘手的,我建议不要在生产中使用,并警告用户这不是推荐的做法。
这并不能完全解决您的问题,您需要在主线程之外运行事物,我不确定这是否可能。
你也可以尝试使用其他循环运行库,例如triocurio;它们可能允许您执行默认情况下无法执行的嵌套操作,但这里有危险。我强烈推荐trio和多篇关于其创建的博客文章,特别是如果您正在教授异步编程。

祝您愉快,并希望这有所帮助,请报告错误以及有效的内容。


1
您可以使用%%script --bg魔术命令使飓风服务器在后台运行。选项--bg告诉jupyter在后台运行当前单元格的代码。
只需在一个单元格中创建一个飓风服务器和魔术命令,并运行该单元格。
示例:
%%script python --bg

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

loop = tornado.ioloop.IOLoop.current()

app = make_app()
app.listen(8000) # 8888 was being used by jupyter in my case

loop.start()

然后您可以在另一个单元格中使用requests连接到服务器:

import requests

print(requests.get("http://localhost:8000"))

# prints <Response [200]>

这里需要注意的一点是,如果你停止/中断任何单元格上的内核,后台脚本也会停止。因此,你需要再次运行此单元格以启动服务器。


这个确实很有效。我认为这属于“命令行子进程”,因为这实际上是在后台发生的。然而,在这种特定情况下,我想避免使用这种解决方案,因为 app/loop 在此情况下不是交互式的。 - Daniel
@Daniel,您能否定义“非交互式”? - xyres
app 无法从 Jupyter 的其他部分访问。例如,如果我有一个方法 app.await_results(),它将阻塞直到操作完成,这在当前模型下是不可能的。 - Daniel

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