Python2 里 print 的原子性
这是一段多线程代码,考虑下执行后会输出什么样的内容:
import threading def test(): a = 'hello' b = 'world' print a, b if __name__ == '__main__': threads = list() for i in xrange(10): t = threading.Thread(target=test) threads.append(t) for thread in threads: thread.start() for thread in threads: thread.join()
它的本意是让10个线程打印“hello world”的,但是执行后,感觉有点不对劲:
$ python test.py hello world hello world hello world hello hello world world hello world hello world hello world hello world hello world
导致打印出的文字行歪歪扭扭的原因是发生了线程切换,Python 的多线程依赖 GIL 机制,这方面更详细的你可以去参考别的文档,在这里只用知道:在“当前线程”获得执行时,GIL 会锁上,然后其他线程不可打断当前的执行,直到 GIL 被解锁后方可切换。而解锁的条件大概有两个(还有其他可能性,为了不陷入这个细节,我们只谈论常见的两个):1、发生 I/O 操作,当前线程需要等待,则解锁 GIL,其他线程继续执行;2、通常情况下,GIL 执行了 N 条字节码后(N 默认是100,这个数字可以修改的),发生切换。
要想弄明白歪歪扭扭的原因,得先看看 test 函数对应的字节码。单独把 test 函数的内容提取到一个新的文件中(多线程的代码不方便取一个函数的字节码),然后查看它对应的字节码:
$ python -m dis x.py 1 0 LOAD_CONST 0 ('hello') 3 STORE_NAME 0 (a) 2 6 LOAD_CONST 1 ('world') 9 STORE_NAME 1 (b) 3 12 LOAD_NAME 0 (a) 15 PRINT_ITEM 16 LOAD_NAME 1 (b) 19 PRINT_ITEM 20 PRINT_NEWLINE 21 LOAD_CONST 2 (None) 24 RETURN_VALUE
PRINT_ITEM 字节码对应的就是 print 函数,对于 print a,b 这条语句,其实执行了两次 PRINT_ITEM,也就是说 print a,b 等于:
print a print b
并且,print a,b 之后还要显示一行换行符,实际上等于:
print a, # 如不明白为何要在尾巴多个逗号,那么该复习 Python 了 print b, print '\n'
在上面字节码中,换行对应的字节码是 PRINT_NEWLINE。就是说这里为了打印 a 和 b 变量,大约执行了 3 条主要的字节码。
所以,假设当前脚本执行 100 个字节码后便发生线程切换,而打印 a 变量的字节码正好是第一百条,在打印了 a 后,发生了线程切换,待下次该线程获得了执行时间片时,再去打印b,这时看到的格式绝对是混乱的;如果是一次把 a 和 b 都打印出来了,但还没来得及打印换行符,又发生了切换,而这个换行符又要等到下次有机会时再打印了,这样打印出来的东西又是错乱的;运气好点的话,a、b 和换行是一口气执行完的,而我们需要的就是把这种运气成分变成百分百可行的方法,这种一气呵成干完活的需求便是“保持原子性”,原子性简单说就是保证某些操作是连贯的、不被打断的、一口气干到底干完的。这里的连贯操作就是需要一气呵成完成:1、打印变量 a;2、打印变量 b;3、打印换行符。
当然,你可能会尝试用锁来解决这个问题,但是锁的开销太大了。
其实要是能让 print 一口气把 a、b 和换行符都打印了就可以:
print '%s %s\n;'%(a, b),
以上代码可以保证原子性,因为 print 最终只打印一个字符串——而不是分成 3 条打印的。换行符是嵌入在字符串中的,由终端来显示换行,而不是单独执行 PRINT_NEWLINE 字节码:
1 0 LOAD_CONST 0 ('hello') 3 STORE_NAME 0 (a) 2 6 LOAD_CONST 1 ('world') 9 STORE_NAME 1 (b) 3 12 LOAD_CONST 2 ('%s %s\n') 15 LOAD_NAME 0 (a) 18 LOAD_NAME 1 (b) 21 BUILD_TUPLE 2 24 BINARY_MODULO 25 PRINT_ITEM 26 LOAD_CONST 3 (None)
可以看到 PRINT_ITEM 只执行了一次,所以轮到它执行时,它能保证完成输出这行字符串。而前面把变量拼成字符串虽然花了几条指令,但不影响正常显示,因为无论花费了多少条字节码,这些都不影响最终完整打印的。
另外,使用 logging 一类的日志模块打印也可以保证原子性的。