本文来自依云's Blog,转载请注明。
>>> python -m this | grep -A1 -F Errors Errors should never pass silently. Unless explicitly silenced.
因为要捕获子进程的标准输出、标准错误以及退出状态码,用 callback 写会非常麻烦,因为三者全部完成才能进行下一步操作。而使用 Tornado 的 coroutine 就很方便了,示例如下:
from tornado.gen import coroutine, Task from tornado.process import Subprocess @coroutine def run_cmd(cmd): p = Subprocess( cmd, stdout = Subprocess.STREAM, stderr = Subprocess.STREAM, ) out, err, code = yield [Task(p.stdout.read_until_close), Task(p.stderr.read_until_close), Task(p.set_exit_callback)] return out, err, code # For Python below 3.3, use # raise Return((out, err, code))
yield 一个 Task(或者 Future)的列表的话,它们会并发执行,全部执行完毕之后才会返回到这个 yield 位置继续执行。简洁干净。(不过我要吐槽一下为什么必须传列表,传元组就不对……)
于是乎,调用各种外部命令的部分被我由一堆回调改成了 coroutine,除了 yield 关键字有些别扭外,整个代码可读性好多了 :-)
可是后来,发生了这样的一件事:通过日志能看到一个 coroutine 前边的代码执行了,而后边的代码却没有执行,中间也没有 yield 到别的地方去!看上去非常诡异。
恰好前些天刚好看到一很不错的 Python 调试器 pudb。于是去执行中断的地方打断点(import pudb; pu.db
),然后单步跟踪。这才发现原来是中间有个语句抛出了异常,然后这个异常被 coroutine「吃掉」了……示例代码如下:
#!/usr/bin/env python3 from tornado.gen import coroutine from tornado.ioloop import IOLoop @coroutine def two(): print('two entered') 1 / 0 print('two leaving') @coroutine def one(): print('one entered') yield two() print('one leaving') if __name__ == '__main__': one() IOLoop.current().start()
结果是:
one entered two entered
执行从发生异常的那个位置中断了,并且没有任何错误消息被记录。(PS: 要是在 coroutine 里使用 try...except
的话是能抓到它的。)
以「tornado coroutine exception」为关键字找到了这个以及这个。原来 coroutine 的异常是被它返回的那个 Future 对象「吃掉」了。如果是在 Tornado 的 HTTP 服务里(RequestHandler
),Tornado 的 web 模块会处理并记录这种异常。然而我是在 web 模块之外使用的,所以得自己来处理了:
#!/usr/bin/env python3 from tornado.gen import coroutine from tornado.ioloop import IOLoop @coroutine def two(): print('two entered') 1 / 0 print('two leaving') @coroutine def one(): print('one entered') yield two() print('one leaving') def _future_done(fu): fu.result() if __name__ == '__main__': fu = one() fu.add_done_callback(_future_done) IOLoop.current().start()
这样就能看到有异常发生了:
one entered two entered ERROR:concurrent.futures:exception calling callback for <Future at 0x7f286c0bcf90 state=finished raised ZeroDivisionError> ... File "t.py", line 9, in two 1 / 0 ZeroDivisionError: division by zero
那个异常的 Traceback 很长很长。没有原生的良好的协程支持的代价吧,不知道 Python 3.4 的 asyncio 里会不会好一些。
2014年8月2日更新:asyncio 在遇到这种情况时会打印错误日志,参见文档。
Jan 15, 2014 08:28:25 PM
Tornado为了协程正确返回异常可是做了不少努力呢。
看源代码就知道了。
要怪还得怪当年Python语言本身没有很好的支持。
看现在Python 3.3一上yield from,Python 3.4一上asyncio,什么都改善了。
Jan 16, 2014 12:01:42 AM
是呢。不过感觉 yield from 的能力还是没有 greenlet 或者 Lua 的 coroutine 强。
Jan 16, 2014 12:29:04 AM
全要coroutine就直接上Stackless Python好了……
Jan 16, 2014 11:50:23 PM
最近做的个应用也是各种subprocess,依旧用的tornado,还好是个单用户情景,sleep也用了,阻塞就阻塞吧,就差到处开线程包装了=w=
Jan 17, 2014 12:09:17 AM
竟然还用 sleep……add_timeout 挺好的呀。
Jan 17, 2014 12:29:50 AM
刚试着用addtimeout,效果还不错,是这么个情形,子程序运行60s就砍死,
但是貌似subprocess还是会阻塞的样子呢,果然还是得用个thread包着_(:3 」∠)_
-所以效果一样啦-
Jan 17, 2014 12:26:33 PM
你没用 tornado.process.Subprocess 么?
Jan 17, 2014 01:44:30 PM
这个bug报告了么?快报告嘛……
Jan 18, 2014 12:55:05 PM
就用的自带的那个嘛,才发现仙子也是用的tornado.process, Tanks =3=
Jan 18, 2014 02:01:41 PM
pudb好棒的说!
Jan 20, 2014 03:48:04 PM
这就是为什么你应该使用Twisted
Jan 20, 2014 04:19:17 PM
Twisted 不支持 Python 3。