全局解釋器鎖--GIL
參考博客:https://www.cnblogs.com/mindsbook/archive/2009/10/15/thread-safety-and-GIL.html
https://www.cnblogs.com/MnCu8261/p/6357633.html
http://python.jobbole.com/87743/
一、前言
在多核cpu的背景下,基於多線程以充分利用硬件資源的編程方式也不斷發展,也就是在同一時間,可以運行多個任務。但是Cpython中由於GIL的存在,導致同一時間內只有一個線程在運行。GIL的全稱為Global Interpreter Lock,也就是全局解釋器鎖。存在在Python語言的主流執行環境Cpython中,GIL是一個真正的全局線程排他鎖,在解釋器執行任何Python代碼時,都需要獲得這把GIL鎖。雖然 CPython 的線程庫直接封裝操作系統的原生線程,但 CPython 進程做為一個整體,同一時間只會有一個獲得了 GIL 的線程在跑,其它的線程都處於等待狀態等著 GIL 的釋放。GIL 直接導致 CPython 不能利用物理多核的性能加速運算。
不同的線程也是被分配到不同的核上面運行的,但是同一時間只有一個線程在運行
二、為什麽存在GIL
2.1 線程安全
想要利用多核的優勢,我們可以采用多進程或者是多線程,兩者的區別是資源是否共享。前者是獨立的,而後者是共享的。相對於進程而言,多線程環境最大的問題是如果保證資源競爭、死鎖、數據修改等。於是就有了線程安全。
線程安全 是在多線程的環境下, 線程安全能夠保證多個線程同時執行時程序依舊運行正確, 而且要保證對於共享的數據,可以由多個線程存取,但是同一時刻只能有一個線程進行存取.
既然,多線程環境下必須存在資源的競爭,那麽如何才能保證同一時刻只有一個線程對共享資源進行存取?
加鎖, 加鎖可以保證存取操作的唯一性, 從而保證同一時刻只有一個線程對共享數據存取。
通常加鎖也有2種不同的粒度的鎖:
- fine-grained(所謂的細粒度), 那麽程序員需要自行地加,解鎖來保證線程安全
- coarse-grained(所謂的粗粒度), 那麽語言層面本身維護著一個全局的鎖機制,用來保證線程安全
前一種方式比較典型的是 java, Jython 等, 後一種方式比較典型的是 CPython (即Python)。
2.2 Python自身特點
依照Python自身的哲學, 簡單 是一個很重要的原則,所以, 使用 GIL 也是很好理解的。多核 CPU 在 1990 年代還屬於類科幻,Guido van Rossum 在創造 python 的時候,也想不到他的語言有一天會被用到很可能 多核的 CPU 上面,一個全局鎖搞定多線程安全在那個時代應該是最簡單經濟的設計了。簡單而又能滿足需求,那就是合適的設計(對設計來說,應該只有合適與否,而沒有好與不好)。
三、線程切換
一個線程無論何時開始睡眠或等待網絡 I/O,其他線程總有機會獲取 GIL 執行 Python 代碼。這是協同式多任務處理。CPython 也還有搶占式多任務處理。如果一個線程不間斷地在 Python 2 中運行 100次指令,或者不間斷地在 Python 3 運行15 毫秒,那麽它便會放棄 GIL,而其他線程可以運行。
3.1 協同式多任務處理
當一項任務比如網絡 I/O啟動,而在長的或不確定的時間,沒有運行任何 Python 代碼的需要,一個線程便會讓出GIL,從而其他線程可以獲取 GIL 而運行 Python。這種禮貌行為稱為協同式多任務處理,它允許並發,多個線程同時等待不同事件。
def do_connect(): s = socket.socket() s.connect((‘python.org‘, 80)) # drop the GIL for i in range(2): t = threading.Thread(target=do_connect) t.start()
兩個線程在同一時刻只能有一個執行 Python ,但一旦線程開始連接,它就會放棄 GIL ,這樣其他線程就可以運行。這意味著兩個線程可以並發等待套接字連接,這是一件好事。在同樣的時間內它們可以做更多的工作。
3.2 搶占式多任務處理
如果沒有I/O中斷,而是CPU密集型的的程序,解釋器運行一段時間就會放棄GIL,而不需要經過正在執行代碼的線程允許,這樣其他線程便能運行。在python3中,這個時間間隔是15毫秒。
四、Python中的線程安全
如果一個線程可以隨時失去 GIL,你必須使讓代碼線程安全。 然而 Python 程序員對線程安全的看法大不同於 C 或者 Java 程序員,因為許多 Python 操作是原子的。
在列表中調用 sort(),就是原子操作的例子。線程不能在排序期間被打斷,其他線程從來看不到列表排序的部分,也不會在列表排序之前看到過期的數據。原子操作簡化了我們的生活,但也有意外。例如,+ = 似乎比 sort() 函數簡單,但+ =不是原子操作。
在python 2中(python3中結果沒有問題):
# -*- coding: UTF-8 -*- import time import threading n = 0 def add_num(): global n time.sleep(1) n += 1 if __name__ == ‘__main__‘: thread_list = [] for i in range(100): t = threading.Thread(target=add_num) t.start() thread_list.append(t) for t in thread_list: t.join() print ‘final num:‘, n
輸出:
[root@MySQL ~]# python mutex.py final num: 98 [root@MySQL ~]# python mutex.py final num: 100 [root@MySQL ~]# python mutex.py final num: 96 [root@MySQL ~]# python mutex.py final num: 99 [root@MySQL ~]# python mutex.py final num: 100
得到的結果本來應該是100,但是實際上並不一定。
原因就在於,運行中有線程切換發生,一個線程失去了GIL,當一個線程A獲取n = 43時,還沒有完成n +=1這個操作,就失去了GIL,此時正好另一個線程B獲取了GIL,並也獲取了 n = 43,B完成操作後,n = 44。可是先前那個線程A又獲得了GIL,又開始運行,最後也完成操作 n = 44。所有最後的結果就會出現偏差。
上圖就是n += 1運行到一半時失去GIL後又獲得GIL的過程。
五、Mutex排他鎖
如何解決上面的偏差,保證結果的正確性?其實我們要做的就是確保每一次的運行過程是完整的,就是每次線程在獲取GIL後,要將得到的共享數據計算完成後,再釋放GIL鎖。那又如何能做到這點呢?還是加鎖,給運行的程序加鎖,就能確保在程序運行時,必須完全運行完畢。
# -*- coding: UTF-8 -*- import time import threading n = 0 lock = threading.Lock() # 添加一個鎖的實例 def add_num(): global n with lock: # 獲取鎖 n += 1 if __name__ == ‘__main__‘: thread_list = [] for i in range(100): t = threading.Thread(target=add_num) t.start() thread_list.append(t) for t in thread_list: t.join() # 主線程等待所有線程執行完畢 print ‘final num:‘, n
註:給程序加鎖,程序就變成串行的了。所以程序中不能有sleep,同樣數據量也不能特別大,否則會影響效率
全局解釋器鎖--GIL