IPython Notebook中输入的代码经由浏览器发送给Web服务器,再由Web服务器发送消息到IPython的Kernel执行代码,在Kernel中执行代码所产生的输出会再发送给Web服务器从而发送给浏览器,完成整个运行过程。Web服务器和Kernel之间采用ZeroMQ进行通信。下面为其通信的示意图:
图中,Kernel经由绿色的DEAL-ROUTER通道接收来自Web服务器的命令消息,并返回应答消息。通过红色的PUB-SUB通道传输被执行代码所产生的输出信息。
在Kernel中,用户代码在一个用户环境(字典)中执行,通常无法获得关于Kernel的信息。但是由于用户代码和Kernel在同一进程中执行,因此我们可以通过一些特殊的代码研究Kernel是如何接收、运行并返回消息的。
Kernel中的Socket对象
我们可以通过gc模块的get_objects()
遍历进程中所有的对象,找到我们需要的对象:
import gc def get_objects(class_name): return [o for o in gc.get_objects() if type(o).__name__ == class_name] kapp = get_objects("IPKernelApp")[0]
Kernel的最上层是一个IPKernelApp对象,上面我们通过get_objects()
找到它。它的shell_socket
和iopub_socket
分别用于接收命令和广播代码执行输出,对应于图中的绿色和红色端口。
kapp.shell_socket, kapp.iopub_socket
(<zmq.core.socket.Socket at 0xa59232c>, <zmq.core.socket.Socket at 0xabf586c>)
在Notebook中执行print
时,会经由iopub_socket
将输出的内容传送给Web服务器,最终在Notebook界面中显示。print
语句实际上会调用sys.stdout
完成输出工作。让我们看看Kernel中的sys.stdout
是什么对象:
import sys print sys.stdout print sys.stdout.pub_socket
<span style="font-size: small;"><IPython.zmq.iostream.OutStream object at 0xac1020c> <zmq.core.socket.Socket object at 0xabf586c> </span>
可以看出sys.stdout
是一个对kapp.iopub_socket
进行包装的OutStream
对象。下面是输出错误信息的sys.stderr
的内容,可以看出它和sys.stdout
使用同一个Socket对象。
<span style="font-size: small;">print sys.stdout print sys.stderr.pub_socket </span>
<span style="font-size: small;"><IPython.zmq.iostream.OutStream object at 0xac1020c> <zmq.core.socket.Socket object at 0xabf586c> </span>
Kernel中的线程
下面让我们看看Kernel中的各个线程。通过threading.enumerate()
可以获得当前进程中的所有线程:
<span style="font-size: small;">import threading threading.enumerate() </span>
<span style="font-size: small;">[<_MainThread(MainThread, started -1219512640)>, <Heartbeat(Thread-2, started daemon -1267692736)>, <ParentPollerUnix(Thread-1, started daemon -1288012992)>, <HistorySavingThread(Thread-3, started -1279263936)>]</span>
下面是各个线程所完成的工作:
- 主线程(MainThread)接收来自前端的命令,执行用户代码,并输出代码的执行结果。
- Heartbeat线程用于定时向前端发送消息,让前端知道Kernel是否还活着。如果由于某些用户代码造成Kernel进程崩溃,前端将接收不到来自Heartbeat线程的消息,从而知道Kernel已经被终止了。
- ParentPollerUnix线程,监视父进程,如果父进程退出,则保证Kernel进程也退出。这样当用户关闭前端进程时,Kernel进程能正常结束。
- HistorySaving线程用户将用户输入的历史保存到Sqlite数据库中。
只需要在IPython代码中搜索Heartbeat、ParentPollerUnix和HistorySaving等,就可以找到这些线程的代码,这里就不再多做分析了。下面让我们着重看看主线程是如何执行用户代码的。
用户代码的执行
我们可以通过在用户代码中执行traceback.print_stack()
输出整个执行堆栈:
<span style="font-size: small;">import traceback traceback.print_stack() </span>
<span style="font-size: small;"> File "<string>", line 1, in <module> File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 928, in main app.start() File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/kernelapp.py", line 348, in start ioloop.IOLoop.instance().start() File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/ioloop.py", line 340, in start self._handlers[fd](fd, events) File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/zmqstream.py", line 420, in _handle_events self._handle_recv() File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/zmqstream.py", line 452, in _handle_recv self._run_callback(callback, msg) File "/usr/local/lib/python2.7/dist-packages/zmq/eventloop/zmqstream.py", line 394, in _run_callback callback(*args, **kwargs) File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 268, in dispatcher return self.dispatch_shell(stream, msg) File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 236, in dispatch_shell handler(stream, idents, msg) File "/usr/local/lib/python2.7/dist-packages/IPython/zmq/ipkernel.py", line 371, in execute_request shell.run_cell(code, store_history=store_history, silent=silent) File "/usr/local/lib/python2.7/dist-packages/IPython/core/interactiveshell.py", line 2612, in run_cell interactivity=interactivity) File "/usr/local/lib/python2.7/dist-packages/IPython/core/interactiveshell.py", line 2691, in run_ast_nodes if self.run_code(code): File "/usr/local/lib/python2.7/dist-packages/IPython/core/interactiveshell.py", line 2741, in run_code exec code_obj in self.user_global_ns, self.user_ns File "<ipython-input-22-2e94e8c65f66>", line 2, in <module> traceback.print_stack() </span>
通过这个执行堆栈,我们可以看到用户代码是如何被调用的。
首先,在KernelApp
对象的start()
中,调用ZeroMQ中的ioloop.start()
处理来自shell_socket
的消息。当从Web服务器接收到execute_request
消息时,将调用kernel.execute_request()
方法。
<span style="font-size: small;">kapp.kernel.execute_request </span>
<span style="font-size: small;"><bound method Kernel.execute_request of <IPython.zmq.ipkernel.Kernel object at 0xac10a4c>></span>
在execute_request()
中调用shell对象的如下方法最终执行用户代码:
<span style="font-size: small;">print kapp.kernel.shell.run_cell print kapp.kernel.shell.run_ast_nodes print kapp.kernel.shell.run_code </span>
<span style="font-size: small;"><bound method ZMQInteractiveShell.run_cell of <IPython.zmq.zmqshell.ZMQInteractiveShell object at 0xac10a8c>> <bound method ZMQInteractiveShell.run_ast_nodes of <IPython.zmq.zmqshell.ZMQInteractiveShell object at 0xac10a8c>> <bound method ZMQInteractiveShell.run_code of <IPython.zmq.zmqshell.ZMQInteractiveShell object at 0xac10a8c>> </span>
shell对象在其user_global_ns
和user_ns
属性在执行代码,这两个字典就是用户代码的执行环境,实际上它们是同一个字典:
<span style="font-size: small;">print globals() is kapp.kernel.shell.user_global_ns print globals() is kapp.kernel.shell.user_ns </span>
<span style="font-size: small;">True True </span>
查看shell_socket的消息
我们还可以利用inspect.stack()
获得前面的执行堆栈中的各个frame对象,从而查看堆栈中的局域变量的内容,这样可以观察到Kernel经由shell_socket接收的回送的消息:
下面是Kernel接收到的消息:
<span style="font-size: small;">frames["request"].f_locals["msg"] </span>
<span style="font-size: small;">{'buffers': [], 'content': {u'allow_stdin': False, u'code': u'import inspect\nframes = {}\nfor info in inspect.stack():\n if info[3] == "dispatch_shell":\n frames["request"] = info[0]\n if info[3] == "execute_request":\n frames["reply"] = info[0]', u'silent': False, u'user_expressions': {}, u'user_variables': []}, 'header': {u'msg_id': u'EAB8E58D574B406C9B8C4C7954A677E3', u'msg_type': u'execute_request', u'session': u'4C4B18395EF44483B912275A5F036290', u'username': u'username'}, 'metadata': {}, 'msg_id': u'EAB8E58D574B406C9B8C4C7954A677E3', 'msg_type': u'execute_request', 'parent_header': {}}</span>
下面是Kernel对上述消息的应答:
<span style="font-size: small;">frames["reply"].f_locals["reply_msg"] </span>
<span style="font-size: small;">{'content': {'execution_count': 31, 'payload': [], 'status': u'ok', 'user_expressions': {}, 'user_variables': {}}, 'header': {'date': datetime.datetime(2012, 11, 25, 17, 13, 18, 21057), 'msg_id': '2963ebb0-cbed-48d7-b833-9b6a829b03bd', 'msg_type': u'execute_reply', 'session': u'eac59565-34ff-4618-b92c-8fbbdad75482', 'username': u'kernel', 'version': [0, 14, 0, 'dev']}, 'metadata': {'dependencies_met': True, 'engine': u'10aa4832-7ff8-4420-b4c7-8f33b0914a2b', 'started': datetime.datetime(2012, 11, 25, 17, 13, 18, 1414), 'status': u'ok'}, 'msg_id': '2963ebb0-cbed-48d7-b833-9b6a829b03bd', 'msg_type': u'execute_reply', 'parent_header': {u'msg_id': u'EAB8E58D574B406C9B8C4C7954A677E3', u'msg_type': u'execute_request', u'session': u'4C4B18395EF44483B912275A5F036290', u'username': u'username'}, 'tracker': <zmq.core.message.MessageTracker at 0xa7a51ac>}</span>
注意上面的应答消息并非代码的执行结果,代码的输出在执行代码时已经经由sys.stdout
->iopub_socket
发送给Web服务器了。
小结
我们通过Python的各种标准库:gc, threading, traceback, inspect
查看了Kernel是如何接收和发送消息,以及如何运行用户代码的。更详细的信息请读者参考IPython的开发文档。
通过研究Kernel的工作原理和源代码,我们可以学习到如何使用ZeroMQ制作多进程通信的分布式应用程序。
转自:http://tech.chinaitlab.com/top/909211.html
Ipython notebook的搭建参考:http://jnglingshu.duapp.com/?p=4190