現(xiàn)象
看如下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()
運(yùn)行下輸出會錯亂
這可能是初次接觸python多線程的人經(jīng)常會遇到的問題瓣距。
結(jié)論
先說結(jié)論:Python2的print關(guān)鍵詞不是線程安全的黔帕,Python3的print函數(shù)是線程安全的。
原因
我們用dis包對某個PyObject做反匯編(disassemble)蹈丸,這里的反匯編其實指的是把python腳本翻譯成python中間字節(jié)碼成黄,由python虛擬機(jī)解釋執(zhí)行字節(jié)碼。虛擬機(jī)模擬了cpu逻杖,字節(jié)碼(opcode)則類似匯編語言奋岁。字節(jié)碼解釋器是python的核心所在,Python通過GIL(Global Interpreter Lock)這個互斥鎖保證同一時刻只有一個線程使用解釋器(或者說虛擬機(jī))荸百。
import dis
def a():
print 'hello'
dis.dis(a)
可以看到a()
的字節(jié)碼如下:
其中
print
對應(yīng)兩個字節(jié)碼PRINT_ITEM
和PRINT_NEWLINE
闻伶,分別是輸出對象和輸出換行符,理論上print 'hello'
等價于
sys.stdout.write('hello')
sys.stdout.write('\n')
所以當(dāng)虛擬機(jī)做線程切換時可能會把輸出hello
和輸出\n
的兩個操作切開够话,造成輸出上述錯亂的結(jié)果蓝翰,這就是為什么python2的print不是原子操作。
那我們再看如下代碼及輸出:
import dis
import sys
def a():
sys.stdout.write('hello'+'\n')
dis.dis(a)
其實
write()
函數(shù)的調(diào)用是CALL_FUNCTION
這一步女嘲,而write()
是builtin_function畜份,是原子操作。(可以試試看dis.dis(sys.stdout.write)
)
如何避免
- 使用python3欣尼,社區(qū)將在2020年放棄維護(hù)python2爆雹。
- 如果需要使用python2,需要將多線程中的
print content
替換成sys.stdout.write('{}%\n'.format(content))