现象
看如下python2代码:
import time
import threading
def a():
time.sleep(1)
print 'hello'
ts = []
for i in range(10):
t = threading.Thread(target=a)
t.start()
ts.append(t)
for t in ts:
t.join()
运行下输出会错乱
这可能是初次接触python多线程的人经常会遇到的问题。
结论
先说结论:Python2的print关键词不是线程安全的,Python3的print函数是线程安全的。
原因
我们用dis包对某个PyObject做反汇编(disassemble),这里的反汇编其实指的是把python脚本翻译成python中间字节码,由python虚拟机解释执行字节码。虚拟机模拟了cpu,字节码(opcode)则类似汇编语言。字节码解释器是python的核心所在,Python通过GIL(Global Interpreter Lock)这个互斥锁保证同一时刻只有一个线程使用解释器(或者说虚拟机)。
import dis
def a():
print 'hello'
dis.dis(a)
可以看到a()
的字节码如下:
其中
print
对应两个字节码PRINT_ITEM
和PRINT_NEWLINE
,分别是输出对象和输出换行符,理论上print 'hello'
等价于
sys.stdout.write('hello')
sys.stdout.write('\n')
所以当虚拟机做线程切换时可能会把输出hello
和输出\n
的两个操作切开,造成输出上述错乱的结果,这就是为什么python2的print不是原子操作。
那我们再看如下代码及输出:
import dis
import sys
def a():
sys.stdout.write('hello'+'\n')
dis.dis(a)
其实
write()
函数的调用是CALL_FUNCTION
这一步,而write()
是builtin_function,是原子操作。(可以试试看dis.dis(sys.stdout.write)
)
如何避免
- 使用python3,社区将在2020年放弃维护python2。
- 如果需要使用python2,需要将多线程中的
print content
替换成sys.stdout.write('{}%\n'.format(content))