最近在學(xué)習(xí)Python的多線程編程邮丰,寫幾篇文章記錄一下吻商。
GIL是Global Interpreter Lock,即全局解釋鎖的縮寫漱凝,保證了了同一時(shí)刻只有一個(gè)線程在一個(gè)CPU上執(zhí)行字節(jié)碼纸兔,無法將多個(gè)線程映射到多個(gè)CPU上惰瓜。這是CPython解釋器的缺陷,由于CPython是大部分環(huán)境下默認(rèn)的Python執(zhí)行環(huán)境汉矿,而很多庫都是基于CPython編寫的崎坊,因此很多人將GIL歸結(jié)為Python的問題。
GIL被設(shè)計(jì)來保護(hù)線程安全洲拇,由于多線程共享變量奈揍,如果不能很好的進(jìn)行線程同步,多線程非常容易將線程改亂赋续。實(shí)際上即使有了GIL男翰,這個(gè)問題也無法完全解決,因?yàn)镚IL實(shí)際上也會(huì)釋放纽乱,而且它并不是在某個(gè)線程執(zhí)行完成后才釋放蛾绎,而是根據(jù)代碼的字節(jié)碼或者時(shí)間片進(jìn)行釋放,下面是一個(gè)例子:
import threading
total = 0
def add():
global total
for i in range(1000000):
total += 1
def desc():
global total
for i in range(1000000):
total -= 1
thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(total)
這個(gè)程序直觀來看迫淹,是將total
加1000000減1000000秘通,不管哪個(gè)線程先執(zhí)行为严,最后的結(jié)果應(yīng)該都是0才對敛熬,但是如果允許你該上面的代碼多次,就會(huì)發(fā)現(xiàn)每次代碼的結(jié)果都不一樣第股,有正有負(fù)应民。這其中的原因就涉及到了GIL的釋放。我們首先可以查看一下普通加法函數(shù)的字節(jié)碼:
import dis
def add1(a):
a += 1
return a
print(dis.dis(add1))
結(jié)果如下:
2 0 LOAD_FAST 0 (a)
2 LOAD_CONST 1 (1)
4 INPLACE_ADD
6 STORE_FAST 0 (a)
3 8 LOAD_FAST 0 (a)
10 RETURN_VALUE
None
可以看到a += 1
的執(zhí)行過程是先將變量a
裝載進(jìn)CPU,再將常量1
裝載進(jìn)CPU诲锹,然后執(zhí)行相加操作繁仁,最后再將a
存儲(chǔ)在內(nèi)存中。由于GIL不是根據(jù)Python代碼段來釋放归园,而是根據(jù)字節(jié)碼或者時(shí)間片來釋放的黄虱,在之前的例子中,如果add
函數(shù)在進(jìn)行加法后還未在內(nèi)存中保存庸诱,GIL釋放捻浦,desc
函數(shù)獲得執(zhí)行權(quán),此時(shí)它進(jìn)行裝載時(shí)裝載的變量total
是未進(jìn)行加法操作的total
桥爽,因此相當(dāng)于之前的add
函數(shù)失去了作用朱灿,在進(jìn)行多次循環(huán)后,程序的運(yùn)行結(jié)果自然不為0钠四。這種情況稱為競態(tài)條件(race condition)盗扒,即使沒有GIL,也會(huì)出現(xiàn)這種問題缀去。解決方法是使用鎖機(jī)制侣灶,將會(huì)在后面的文章中提到。
還有一種條件會(huì)導(dǎo)致GIL釋放朵耕,那就是當(dāng)程序遇到IO操作和time.sleep
將程序阻塞的時(shí)候,因此多線程對于處理IO操作的問題非常有效阎曹。