IPython Notebook中输入的代码经由浏览器发送给Web服务器,再由Web服务器发送消息到IPython的Kernel执行代码,在Kernel中执行代码所产生的输出会再发送给Web服务器从而发送给浏览器,完成整个运行过程。Web服务器和Kernel之间采用进行通信。下面为其通信的示意图:
图中,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
在Notebook中执行print
时,会经由iopub_socket
将输出的内容传送给Web服务器,最终在Notebook界面中显示。print
语句实际上会调用sys.stdout
完成输出工作。让我们看看Kernel中的sys.stdout
是什么对象:
import sys
print sys.stdout
print sys.stdout.pub_socket
可以看出sys.stdout
是一个对kapp.iopub_socket
进行包装的OutStream
对象。下面是输出错误信息的sys.stderr
的内容,可以看出它和sys.stdout
使用同一个Socket对象。
print sys.stdout
print sys.stderr.pub_socket
Kernel中的线程
下面让我们看看Kernel中的各个线程。通过threading.enumerate()
可以获得当前进程中的所有线程:
import threading
threading.enumerate()
下面是各个线程所完成的工作:
- 主线程(MainThread)接收来自前端的命令,执行用户代码,并输出代码的执行结果。
- Heartbeat线程用于定时向前端发送消息,让前端知道Kernel是否还活着。如果由于某些用户代码造成Kernel进程崩溃,前端将接收不到来自Heartbeat线程的消息,从而知道Kernel已经被终止了。
- ParentPollerUnix线程,监视父进程,如果父进程退出,则保证Kernel进程也退出。这样当用户关闭前端进程时,Kernel进程能正常结束。
- HistorySaving线程用户将用户输入的历史保存到Sqlite数据库中。
只需要在IPython代码中搜索Heartbeat、ParentPollerUnix和HistorySaving等,就可以找到这些线程的代码,这里就不再多做分析了。下面让我们着重看看主线程是如何执行用户代码的。
用户代码的执行
我们可以通过在用户代码中执行traceback.print_stack()
输出整个执行堆栈:
import traceback
traceback.print_stack()
通过这个执行堆栈,我们可以看到用户代码是如何被调用的。
首先,在KernelApp
对象的start()
中,调用ZeroMQ中的ioloop.start()
处理来自shell_socket
的消息。当从Web服务器接收到execute_request
消息时,将调用kernel.execute_request()
方法。
kapp.kernel.execute_request
在execute_request()
中调用shell对象的如下方法最终执行用户代码:
print kapp.kernel.shell.run_cell
print kapp.kernel.shell.run_ast_nodes
print kapp.kernel.shell.run_code
shell对象在其user_global_ns
和user_ns
属性在执行代码,这两个字典就是用户代码的执行环境,实际上它们是同一个字典:
print globals() is kapp.kernel.shell.user_global_ns
print globals() is kapp.kernel.shell.user_ns
查看shell_socket的消息
我们还可以利用inspect.stack()
获得前面的执行堆栈中的各个frame对象,从而查看堆栈中的局域变量的内容,这样可以观察到Kernel经由shell_socket接收的回送的消息:
import inspect
frames = {}
for info in inspect.stack():
if info[3] == "dispatch_shell":
frames["request"] = info[0]
if info[3] == "execute_request":
frames["reply"] = info[0]
print "hello world"
下面是Kernel接收到的消息:
frames["request"].f_locals["msg"]
下面是Kernel对上述消息的应答:
frames["reply"].f_locals["reply_msg"]
注意上面的应答消息并非代码的执行结果,代码的输出在执行代码时已经经由sys.stdout
->iopub_socket
发送给Web服务器了。
小结
我们通过Python的各种标准库:gc, threading, traceback, inspect
查看了Kernel是如何接收和发送消息,以及如何运行用户代码的。更详细的信息请读者参考IPython的开发文档。
通过研究Kernel的工作原理和源代码,我们可以学习到如何使用ZeroMQ制作多进程通信的分布式应用程序。