python 基礎(三) 線程 Threading GIL 線程同步

python 基礎(三) 線程 Threading GIL 線程同步

Threading 基本語法

其實和Process類幾乎一樣,簡單看看一個用了四線程模擬搶票的實例:

#-*- utf-8 -*-
from threading import Thread
from random import random
from time import time,sleep

ticket_remain = 400

def getTicket(q):
    global ticket_remain
    for i in range(100):
        ticket_remain-=1

if __name__ == "__main__":
    t1 = Thread(target=getTicket, name="t1", args=(0.4,))
    t2 = Thread(target=getTicket, name="t1", args=(0.4,))
    t3 = Thread(target=getTicket, name="t1", args=(0.4,))
    t4 = Thread(target=getTicket, name="t1", args=(0.4,))

    t1.start()
    t2.start()
    t3.start()
    t4.start()
    t1.join()
    t2.join()
    t3.join()
    t4.join()

    print("ticket remain:",ticket_remain)

理解了語法後我們可以更簡潔一點

#-*- utf-8 -*-
from threading import Thread

ticket_remain = 400

def getTicket(q):
    global ticket_remain
    for i in range(100):
        ticket_remain-=1

if __name__ == "__main__":
    for i in range(4):
        thread = Thread(target=getTicket, name="t"+str(i), args=(0.4,))
        thread.start()
        thread.join()
        print("ticket remain:",ticket_remain)

結果當然是
在這裏插入圖片描述

線程併發具體過程

之前在python 基礎(一)提到過,線程是共用系統資源的,包括CPU的計算資源,其實現形式就是併發concurrent,這個概念之前提到過,只要是單核,或者單個運算單元處理多項運算程序,以輪詢的方式處理的都稱爲併發,這裏一個進程process裏面的多個線程threadings要同時使用一個運算資源,也屬於併發。
上一節提到一個搶票的案例,我們把時間定格在最後兩張票的時候,大概具體是這樣的過程:(t1 線程1,t2 線程2)

線程操作 CPU操作 數據庫n的值
t1獲得CPU使用權 t2就緒掛起 CPU調度t1 n=2
t1執行:n=n-1 執行n-1=0 n=2
t1執行:n=n-1 執行n = 0 n=2
t1確認搶到票了,函數返回 接收函數返回 n=1
t1就緒掛起 t2獲得執行權 CPU調度t2 n=1

其他就和t1 相同套路

我們來看下一個案例:

#-*- utf-8 -*-
from threading import Thread

ticket_remain = 2000000

def getTicket1(q):
    global ticket_remain
    for i in range(1000000):
        ticket_remain-=1
    print(q," ticket remained:", ticket_remain)


def getTicket2(q):
    global ticket_remain
    for i in range(1000000):
        ticket_remain-=1
    print(q," ticket remained:", ticket_remain)

if __name__ == "__main__":

    t1 = Thread(target=getTicket1, name="aa", args=("t1",))
    t1.start()
    t2 = Thread(target=getTicket2, name="aa", args=("t2",))
    t2.start()
    t1.join()
    t2.join()

    print("ticket remain:",ticket_remain)

結果是0?
在這裏插入圖片描述
就好像搶到的是同一張票一般
其實也確實是這樣的 我們來看上一次那個過程表
還是把時間定格在最後一張票的時候,
t1 在cpu的執行順序就是

    ticket_remain-1 = 0
    ticket_remain = 0
    print(q," ticket remained:", ticket_remain)  #意思,確定獲得了票 數據庫可以更新了

大概具體是這樣的過程:(t1 線程1,t2 線程2,n是剩餘票數ticket_remain)

線程操作 CPU操作 數據庫n的值
t1獲得CPU使用權 t2就緒掛起 CPU調度t1 n=1
t1執行:n=n-1 執行n-1=0 n=1
t1執行:n=n-1 執行n = 0 n=1
t2獲得CPU使用權 t1就緒掛起 CPU調度t2 n=1
t2執行:n=n-1 執行n-1=0 n=1
t2執行:n=n-1 執行n = 0 n=1
t2確認搶到票了,print(), 函數返回 接收函數返回 n=0
t1獲得CPU使用權 t2就緒掛起 CPU調度t1 n=0
t1 確認搶到票了,print(), 函數返回 接收函數返回 n=0

線程執行中如果被打斷,那麼再次獲得CPU執行權的時候只會繼續執行未執行完的語句(比如調t1在還沒print的時候被打斷,下次奪回執行權的時候就會在打印)

這裏 t1 t2搶的其實是同一張票,都是最後一張票

那麼 爲啥之前我們只有幾百張票的時候不會有這個問題?
這就是Cpython的一個底層大“BUG”——GIL,其實也應該說是特性,主要是爲了數據安全

GIL Global Interpreter Lock 全局鎖

我們看到,之所以出現這樣的問題,完全是由於,我搶票線程中被打斷了,被阻塞了,那麼如果加一個安全保護機制——,就好像精子卵子結合的時候,卵子接收完一個精子後就鎖住自己不和別的精子結合。這樣用在線程的鎖就是GIL(Global Interpreter Lock)
優點顯而易見,可以避免數據出問題——也就是所謂數據安全
數據安全data security 不是說數據是否被盜用,而是數據是否按照我們的程序,我們的預期,從而有規律的變化。

——好像我還是沒回答爲啥數據量小會導致 上鎖,數據大了就不上鎖
好吧我的鍋:這是因爲,數據量大了他會主動釋放GIL,爲啥呢?
CPython比較機智,當檢測到你線程運算量太大,嚴重延誤後面的線程(我們能夠跑代碼感覺到要等好幾秒),於是取消保護機制,寧可數據錯,也不能太慢。
那,如果我就需要他有些時候數據一定不能錯怎麼辦?他會釋放鎖啊?這時我們得用到一個鎖對象,具體請看下集。

複習 線程同步

那麼有了鎖以後,結合上一節(傳送門:python 基礎(二)阻塞 非阻塞 同步 異步)我們說的同步與異步的知識,我們可以發現:線程搶佔CPU計算資源的時候是,CPU調用線程是異步的(asynchronous),線程執行是非阻塞的 (non-blocking)
然而因爲GIL,每個線程只能一個個執行,也就是 串行順序(serial sequential) 的,CPU調用線程也是 同步的synchronous 這就是所謂 線程同步
這樣確實是安全了,問題是如果遇到大量計算都在一個線程裏面,時間該有多長?另外最大的問題是效率低下,這還算是“多線程”嘛,加上線程start join操作的時間,還不如單線程來得快,而且肯定數據安全:)

所以說,Cpython環境下,默認給每個線程添加的GIL是一個“BUG”
下面是拓展閱讀:

解釋器 編譯器 GIL的來源

什麼是解釋器Interpreter?在這之前我們得看看兩種語言類型。
C++是一套語言(語法)標準,是編譯型語言,以生成可執行文件爲目的,具體可以用不同編譯器complier來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。
Python是 解釋型語言 ,不能生成可執行文件,但是具體可以用不同解釋器Interpreter來編譯成可執行代碼。同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行。其中CPython最普遍,某些場合甚至直接代替python執行環境。

我們的GIL就是CPython底層代碼的產物,所以GIL的鍋不能全甩給Python:)

問題

#-*- utf-8 -*-
from threading import Thread

ticket_remain = 40000000

def getTicket(q):
    global ticket_remain
    for i in range(10000000):
        ticket_remain-=1

if __name__ == "__main__":
    for i in range(4):
        thread = Thread(target=getTicket, name="t"+str(i), args=(0.4,))
        thread.start()
        thread.join()
        print("ticket remain:",ticket_remain)

爲什麼當target執行目標是相同的函數不會出現數據崩掉的問題,數據量比上個例子還要大。按理來說,雖然是相同函數,但是是不同的線程,理應本質相同。
期待大佬回答:)

下集預告

下一站比較輕鬆,因爲硬核部分已經搞定勒。
就是學習如何人爲加鎖(GIL是默認自動加鎖的)
傳送:python 基礎(四)鎖 threading.Lock 死鎖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章