当我们在使用Tornado框架编写程序的时候,使用Tornado自带的调试模式可以让我们的工作轻松不少。因为这样我们就不必要在每次修改程序后手动重启服务器。
开启Tornado的方法就是在实例化Application对象的时候加入"debug=True"参数,示例代码:
app = tornado.web.Application(
handlers=[(r'/', IndexHandler)],
debug=True
)
那么,加入"debug=True"参数后代码实现自动编译的原理是什么呢?查看Tornado web.py的源代码,找到Application类的定义,我们看到Application类的构造函数:
def __init__(self, handlers=None, default_host="", transforms=None,
**settings)
其中:
if self.settings.get('debug'):
self.settings.setdefault('autoreload', True)
self.settings.setdefault('compiled_template_cache', False)
self.settings.setdefault('static_hash_cache', False)
self.settings.setdefault('serve_traceback', True)
# Automatically reload modified modules
if self.settings.get('autoreload'):
from tornado import autoreload
autoreload.start()
可以知道,如果我们在Application实例化的时候加入"debug=True"参数,程序import了autoreload模块,并且调用了autoreload.start()函数来实现自动重载。事实上,我们可以有开启Tornado调试模式的第二种写法:
import tornado.autoreload
app = tornado.web.Application(
handlers=[(r'/', IndexHandler)]
)
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(options.port)
instance = tornado.ioloop.IOLoop.instance()
tornado.autoreload.start(instance)
instance.start()
现在,我们来看一下,autoreload.start()又是如何实现程序的自动重载的。查看Tornado的autoreload.py文件,看到start函数:
def start(io_loop=None, check_time=500):
"""Begins watching source files for changes using the given `.IOLoop`. """
io_loop = io_loop or ioloop.IOLoop.current()
if io_loop in _io_loops:
return
_io_loops[io_loop] = True
if len(_io_loops) > 1:
gen_log.warning("tornado.autoreload started more than once in the same process")
add_reload_hook(functools.partial(io_loop.close, all_fds=True))
modify_times = {}
callback = functools.partial(_reload_on_update, modify_times)
scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
scheduler.start()
函数的功能注释已经写得很清楚了,就是循环监视 '.IOLoop'的源文件,默认时间500ms。这个函数通过使用PeriodicCallback类实例化一个scheduler对象,并且根据PeriodicCallback的特性循环调用一个callback函数。而callback函数则是由偏函数functools.partial以函数_reload_on_update, 集合modify_times为参数生成的(函数式编程的思想)。查看_reload_on_update的源代码:
def _reload_on_update(modify_times):
if _reload_attempted:
# We already tried to reload and it didn't work, so don't try again.
return
if process.task_id() is not None:
# We're in a child process created by fork_processes. If child
# processes restarted themselves, they'd all restart and then
# all call fork_processes again.
return
for module in sys.modules.values():
# Some modules play games with sys.modules (e.g. email/__init__.py
# in the standard library), and occasionally this can cause strange
# failures in getattr. Just ignore anything that's not an ordinary
# module.
if not isinstance(module, types.ModuleType):
continue
path = getattr(module, "__file__", None)
if not path:
continue
if path.endswith(".pyc") or path.endswith(".pyo"):
path = path[:-1]
_check_file(modify_times, path)
for path in _watched_files:
_check_file(modify_times, path)
它用_check_file函数检查sys.modules里的各种py文件,如果文件的os.stat时间戳改变,那么就执行_reload()重新编译加载,_check_file函数代码:
def _check_file(modify_times, path):
try:
modified = os.stat(path).st_mtime
except Exception:
return
if path not in modify_times:
modify_times[path] = modified
return
if modify_times[path] != modified:
gen_log.info("%s modified; restarting server", path)
_reload()
这里,modified = os.stat(path).st_mtime就是获取路径为path的文件最后被修改的时间。
#一个使用os.stat来获取文件创建时间和最近修改时间的小例子
import os
import time
stat = os.stat(path)
print "%s" % time.ctime(stat.st_ctime)
print "%s" % time.ctime(stat.st_mtime)
通过比较在这一次循环中文件的st_mtime与上次循环的st_mtime,来发现文件是否被修改过。如果发现有文件被修改,就restarting server。看到_reload()函数的源代码:
def _reload():
global _reload_attempted
_reload_attempted = True
for fn in _reload_hooks:
fn()
if hasattr(signal, "setitimer"):
# Clear the alarm signal set by
# ioloop.set_blocking_log_threshold so it doesn't fire
# after the exec.
signal.setitimer(signal.ITIMER_REAL, 0, 0)
# sys.path fixes: see comments at top of file. If sys.path[0] is an empty
# string, we were (probably) invoked with -m and the effective path
# is about to change on re-exec. Add the current directory to $PYTHONPATH
# to ensure that the new process sees the same path we did.
path_prefix = '.' + os.pathsep
if (sys.path[0] == '' and
not os.environ.get("PYTHONPATH", "").startswith(path_prefix)):
os.environ["PYTHONPATH"] = (path_prefix +
os.environ.get("PYTHONPATH", ""))
if sys.platform == 'win32':
# os.execv is broken on Windows and can't properly parse command line
# arguments and executable name if they contain whitespaces. subprocess
# fixes that behavior.
subprocess.Popen([sys.executable] + sys.argv)
sys.exit(0)
else:
try:
os.execv(sys.executable, [sys.executable] + sys.argv)
except OSError:
# Mac OS X versions prior to 10.6 do not support execv in
# a process that contains multiple threads. Instead of
# re-executing in the current process, start a new one
# and cause the current process to exit. This isn't
# ideal since the new process is detached from the parent
# terminal and thus cannot easily be killed with ctrl-C,
# but it's better than not being able to autoreload at
# all.
# Unfortunately the errno returned in this case does not
# appear to be consistent, so we can't easily check for
# this error specifically.
os.spawnv(os.P_NOWAIT, sys.executable,
[sys.executable] + sys.argv)
sys.exit(0)
_USAGE = """\
Usage:
python -m tornado.autoreload -m module.to.run [args...]
python -m tornado.autoreload path/to/script.py [args...]
"""
在这个函数中,先执行了_reload_hooks中的函数,也就是ioloop.close,然后对文件的路径进行了判断和相关操作,再判断操作系统,如果sys.platform 为'win32'的话,就执行subprocess.Popen([sys.executable] + sys.argv)进行重载,否则行os.spawnv(os.P_NOWAIT, sys.executable, [sys.executable] + sys.argv)进行重载。
以上大致就是"debug=True"参数的作用原理了。我在解析的过程中,只是简单的根据源码来一步一步追踪 'debug=True'加入后被执行到的函数,由于本人知识水平有限,理解难免有疏漏甚至错误的地方,希望各位多多包涵并为我指出错漏。
附上tornado.autoreload、tornado.ioloop.PeriodicCallback的源码以及几个相关博客链接:
tornado.ioloop.PeriodicCallback
玩蛇记-使用Tornado构建高性能Web之二-autoreload