多執行緒同時存取共享資源(如計數器 counter += 1)時,單一運算實際分三步:讀取、計算、寫回。若執行緒在讀取後被切換,另一執行緒也讀到舊值,最終導致寫回值錯誤,這就是「臟資料」問題。
CPython 的 GIL 保證單一 bytecode 指令不可中斷。以下操作是執行緒安全的:
append、pop、clear、索引存取)注意:「讀取→修改→寫回」的複合操作(如 x = x + 1)不是原子的。
import threading
lock = threading.Lock()
lock.acquire()
try:
# 存取共享資源
finally:
lock.release()
# Python 2.5+ 建議用 with 區塊
with lock:
# 存取共享資源
acquire(blocking=False) 可嘗試非阻塞取得鎖。
同一執行緒可重複 acquire() 不會自我阻塞,解決「外層已持有鎖,內層函式又要鎖」的巢狀呼叫死結。
rlock = threading.RLock()
def outer():
with rlock:
inner() # 內層也用同一把 rlock,不會卡住
def inner():
with rlock:
pass
內部計數器控制同時存取的執行緒數,適合「資源池」場景(如資料庫連線池、HTTP 連線池)。
# 最多 10 條連線
pool_sema = threading.BoundedSemaphore(10)
with pool_sema:
# 使用連線
pass
BoundedSemaphore 會在 release() 次數超過 acquire() 時拋異常,防止邏輯錯誤。
簡單的「等待/通知」機制,一個旗標,多執行緒可 wait() 等待,單一執行緒 set() 喚醒全體。
evt = threading.Event()
# 消費者
evt.wait() # 阻塞直到 set()
# 生產者
evt.set() # 喚醒所有等待者
evt.clear() # 重置旗標
Event 的進階版,綁定一把鎖,支援「等待特定條件」並自動釋放/重新取得鎖,經典「生產者-消費者」模式首選。
cond = threading.Condition()
# 生產者
with cond:
produce_item()
cond.notify() # 或 notifyAll()
# 消費者
with cond:
while not item_available():
cond.wait() # 釋放鎖並阻塞,被喚醒後重新取得鎖
consume_item()
wait(timeout) 可設超時避免永久阻塞。
balance = 0
lock = threading.Lock()
def change(n):
global balance
with lock:
balance += n
balance -= n # 應回到 0
threads = [threading.Thread(target=change, args=(i,)) for i in (5, 8, 9)]
for t in threads: t.start()
for t in threads: t.join()
print(balance) # 穩定輸出 0
未加鎖時,高併發下結果會是隨機非零值。
| 工具 | 適用場景 | 關鍵特性 |
|---|---|---|
| 原子操作 | 單一簡單資料型別 | 無鎖、最高效 |
| Lock | 一般互斥同步 | 不可重入、輕量 |
| RLock | 巢狀/遞迴呼叫同步 | 可重入、追蹤遞迴層級 |
| Semaphore | 限流、資源池 | 計數器、允許 N 並發 |
| Event | 單次廣播通知 | 旗標、無鎖關聯 |
| Condition | 複雜等待/通知協議 | 綁定鎖、支援條件判斷 |