1. 程式人生 > >全局解釋器鎖--GIL

全局解釋器鎖--GIL

print safety 整體 cpu 操作系統 connect png -a add

參考博客: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種不同的粒度的鎖:

  1. fine-grained(所謂的細粒度), 那麽程序員需要自行地加,解鎖來保證線程安全
  2. 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