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中執行多執行緒賦值全域性變數操作時候是真正並行的,存在執行時間先後的問題導致的全域性變數混亂即執行緒非安全
操作的原子性
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