首發于微信公眾號:Python編程時光
在線博客地址:http://python.iswbm.com/en/latest/c01/c01_42.html
在并發編程時,如果多個執行緒訪問同一資源,我們需要保證訪問的時候不會產生沖突,資料修改不會發生錯誤,這就是我們常說的 執行緒安全 ,
那什么情況下,訪問資料時是安全的?什么情況下,訪問資料是不安全的?如何知道你的代碼是否執行緒安全?要如何訪問資料才能保證資料的安全?
本篇文章會一一回答你的問題,
1. 執行緒不安全是怎樣的?
要搞清楚什么是執行緒安全,就要先了解執行緒不安全是什么樣的,
比如下面這段代碼,開啟兩個執行緒,對全域變數 number 各自增 10萬次,每次自增 1,
from threading import Thread, Lock
number = 0
def target():
global number
for _ in range(1000000):
number += 1
thread_01 = Thread(target=target)
thread_02 = Thread(target=target)
thread_01.start()
thread_02.start()
thread_01.join()
thread_02.join()
print(number)
正常我們的預期輸出結果,一個執行緒自增100萬,兩個執行緒就自增 200 萬嘛,輸出肯定為 2000000 ,
可事實卻并不是你想的那樣,不管你運行多少次,每次輸出的結果都會不一樣,而這些輸出結果都有一個特點是,都小于 200 萬,
以下是執行三次的結果
1459782
1379891
1432921
這種現象就是執行緒不安全,究其根因,其實是我們的操作 number += 1 ,不是原子操作,才會導致的執行緒不安全,
2. 什么是原子操作?
原子操作(atomic operation),指不會被執行緒調度機制打斷的操作,這種操作一旦開始,就一直運行到結束,中間不會切換到其他執行緒,
它有點類似資料庫中的 事務,
在 Python 的官方檔案上,列出了一些常見原子操作
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 = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1
像上面的我使用自增操作 number += 1,其實等價于 number = number + 1,可以看到這種可以拆分成多個步驟(先讀取相加再賦值),并不屬于原子操作,
這樣就導致多個執行緒同時讀取時,有可能讀取到同一個 number 值,讀取兩次,卻只加了一次,最終導致自增的次數小于預期,
當我們還是無法確定我們的代碼是否具有原子性的時候,可以嘗試通過 dis 模塊里的 dis 函式來查看

當我們執行這段代碼時,可以看到 number += 1 這一行代碼,由兩條位元組碼實作,
BINARY_ADD:將兩個值相加STORE_GLOBAL: 將相加后的值重新賦值
每一條位元組碼指令都是一個整體,無法分割,他實作的效果也就是我們所說的原子操作,
當一行代碼被分成多條位元組碼指令的時候,就代表在執行緒執行緒切換時,有可能只執行了一條位元組碼指令,此時若這行代碼里有被多個執行緒共享的變數或資源時,并且拆分的多條指令里有對于這個共享變數的寫操作,就會發生資料的沖突,導致資料的不準確,
為了對比,我們從上面串列的原子操作拿一個出來也來試試,是不是真如官網所說的原子操作,
這里我拿字典的 update 操作舉例,代碼和執行程序如下圖

從截圖里可以看到,info.update(new) 雖然也分為好幾個操作
LOAD_GLOBAL:加載全域變數LOAD_ATTR: 加載屬性,獲取 update 方法LOAD_FAST:加載 new 變數CALL_FUNCTION:呼叫函式POP_TOP:執行更新操作
但我們要知道真正會引導資料沖突的,其實不是讀操作,而是寫操作,
上面這么多位元組碼指令,寫操作都只有一個(POP_TOP),因此字典的 update 方法是原子操作,
3. 實作人工原子操作
在多執行緒下,我們并不能保證我們的代碼都具有原子性,因此如何讓我們的代碼變得具有 “原子性” ,就是一件很重要的事,
方法也很簡單,就是當你在訪問一個多執行緒間共享的資源時,加鎖可以實作類似原子操作的效果,一個代碼要嘛不執行,執行了的話就要執行完畢,才能接受執行緒的調度,
因此,我們使用加鎖的方法,對例子一進行一些修改,使其具備原子性,
from threading import Thread, Lock
number = 0
lock = Lock()
def target():
global number
for _ in range(1000000):
with lock:
number += 1
thread_01 = Thread(target=target)
thread_02 = Thread(target=target)
thread_01.start()
thread_02.start()
thread_01.join()
thread_02.join()
print(number)
此時,不管你執行多少遍,輸出都是 2000000.
4. 為什么 Queue 是執行緒安全的?
Python 的 threading 模塊里的訊息通信機制主要有如下三種:
- Event
- Condition
- Queue
使用最多的是 Queue,而我們都知道它是執行緒安全的,當我們對它進行寫入和提取的操作不會被中斷而導致錯誤,這也是我們在使用佇列時,不需要額外加鎖的原因,
他是如何做到的呢?
其根本原因就是 Queue 實作了鎖原語,因此他能像第三節那樣實作人工原子操作,
原語指由若干個機器指令構成的完成某種特定功能的一段程式,具有不可分割性;即原語的執行必須是連續的,在執行程序中不允許被中斷,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/9973.html
標籤:Go
下一篇:如何使用 frp 實作內網穿透
