1. 程式人生 > 實用技巧 >GIL鎖與執行緒安全

GIL鎖與執行緒安全

網上經常看到一些關於執行緒安全的錯誤觀點諸如:Python list、set 等非執行緒安全,而訊息佇列Queue執行緒安全,這是非常危險的大錯特錯的認識!!!

在Python中,執行緒安全是針對操作的原子性的,與物件無關

1.什麼是執行緒安全?

  • 首先,執行緒安全不是針對物件的,所以不能說Queue是執行緒安全的,而另一個物件(如list)是非執行緒安全的
  • 我們以一個demo解釋下什麼是執行緒安全
import threading

number = 0

def add():
    global number
    for i in range(1000000):
        number += 1

thread_1 = threading.Thread(target=add)
thread_2 = threading.Thread(target=add)
thread_1.start()
thread_2.start()

thread_1.join()
thread_2.join()

print(number)

  按照推論應該是累加操作,結果應該是2000000才對,但是實際執行下來多次可以觀察到每次結果不同,而且都小於 200000, 這就是執行緒非安全此處體現為全域性變數資料汙染

  

2.GIL鎖

  • cPython 直譯器中存在大大的全域性直譯器鎖,保證同一時間只有一個執行緒在執行

  • 由此產生個問題: 既然同一時刻只能有一個執行緒執行,那麼為什麼還會出現執行緒非安全

    因為執行緒執行過程中的遇到io操作會,或者連續執行一定數量指令後,會讓出cpu資源,讓其他執行緒使用,即執行緒排程,以上面的程式碼執行流程為例: 當執行緒1執行全域性變數+1的時候執行緒2此時是阻塞等待狀態,cpu排程到執行緒2的時候才執行執行緒2中的全域性變數+1,不像Go中執行多執行緒賦值全域性變數操作時候是真正並行的,存在執行時間先後的問題導致的全域性變數混亂即執行緒非安全

    ,這麼想來,python GIL鎖的存在導致程式執行緒之間一來一回交替執行永遠不會出錯才對,哈哈,那為什麼還會混亂呢?這是因為我們預設number += 1是個原子操作了,即操作的原子性

3.原子操作

  • 原子操作(atomic operation),指不會被執行緒排程機制打斷的操作,這種操作一旦開始,就一直執行到結束,中間不會切換到其他執行緒,它有點類似資料庫中的 事務。

  • 我們列舉一些常見的原子操作

L.append(x) 
L1.extend(L2) 
x = L[i] 
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

  常見的非原子操作

i += 1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1

  如何確定原子操作?

from dis import dis
def func():
    L = []
    L.append(1+1)

dis(func)
    • 當一行程式碼被分成多條位元組碼指令的時候,就代表線上程執行緒切換時,有可能只執行了一條位元組碼指令,此時若這行程式碼裡有被多個執行緒共享的變數或資源時,並且拆分的多條指令裡有對於這個共享變數的寫操作,就會發生資料的衝突,導致資料的不準確。

    • 還是以上面的demo為例:
      當執行緒1執行 到number+=1 時,是分為兩個位元組碼進行的操作,即 1. number+1 2. number賦值,那我們可以想象, 當所有執行緒同時執行,我們假設執行緒1先執行,此時執行緒1執行了一定數量的指令後,假設執行到 number=6後的 number+1(7) 這個原子操作處,此時執行緒2開始執行拿到的 number=6 那麼 執行緒2接著執行累加到number=10,此時 交替到執行緒1 執行 number=7,多次迴圈執行下就會造成這個全域性變數髒資料即執行緒非安全

    • 如何解決呢?
      即強制同步操作,全域性變數非原子操作的地方實行串行同步操作,即,強制原子化,把我們會產生執行緒安全問題的關鍵程式碼加鎖,此時鎖內的程式碼即為一個原子操作,再加上GIL鎖的影響此時執行緒是絕對安全的。

    • 我們對上述程式碼作些修改

import threading

mutex = threading.Lock()
number = 0

def add():
    global number
    for i in range(1000000):
    	with mutex:
        	number += 1

thread_1 = threading.Thread(target=add)
thread_2 = threading.Thread(target=add)
thread_1.start()
thread_2.start()

thread_1.join()
thread_2.join()

print(number)

  此時執行緒絕對安全,最終結果為2000000
Python list、set 等非執行緒安全,而訊息佇列Queue執行緒安全,因為list的append操作也有原子操作,此時不管怎麼追加元素都不會造成資料汙染,如果是 非原子操作的append如L.append(a+b) 此時也是安全的,因為 append內的原子操作不是針對共享變數的的,或者說列表的寫操作安全,內容的安全性在其他地方加鎖保證,總而言之一句話,哪裡有衝突解決哪裡
結論: 執行緒非安全是針對操作的不是針對某一類物件而言,真正引起執行緒非安全的是 寫操作,而非讀操作,確定寫操作安全性才能確保執行緒安全。最後一句,哪裡有衝突解決哪裡!

轉載:https://blog.csdn.net/weixin_43380311/article/details/107686342