Race Condition 是什麼

多個 thread 同時讀寫同一塊記憶體,執行結果取決於 thread 的排程順序——這就是 race condition。

最經典的例子:兩個 thread 同時對 counter += 1

# counter = 100,兩個 thread 各加 1,預期 102,但可能得到 101
 
# Thread A 讀 counter(值:100)
# Thread B 讀 counter(值:100)← B 還沒讀到 A 的更新
# Thread A 寫 counter(101)
# Thread B 寫 counter(101)← B 用舊值 100+1,蓋掉 A 的更新
 
# 結果:101,少了 1

這個 bug 的特性:

  • 單 thread 測試看不到(只有一個 thread 的時候順序固定)
  • 低並發下極少觸發(thread 的時機剛好重疊才會爆)
  • 線上突然爆發(高流量下 thread 衝突頻率急劇上升)

銀行轉帳的 Race Condition

更嚴重的案例——A 轉 100 給 B,同時 C 轉 100 給 B:

def transfer(from_account, to_account, amount):
    if from_account.balance >= amount:  # ← 讀
        from_account.balance -= amount  # ← 寫
        to_account.balance += amount    # ← 寫
 
# A 帳戶:200,轉 150 給 B(Thread 1)
# C 帳戶:200,轉 150 給 B(Thread 2)
 
# Thread 1 讀 A.balance = 200,條件成立
# Thread 2 讀 A.balance = 200,條件成立(A 的扣款還沒寫回)
# Thread 1 寫 A.balance = 50
# Thread 2 寫 A.balance = 50  ← 蓋掉了,A 少扣了 100

解法是把整個 check-then-act 包成 atomic 操作,或用 lock 保護:

import threading
 
lock = threading.Lock()
 
def transfer(from_account, to_account, amount):
    with lock:
        if from_account.balance >= amount:
            from_account.balance -= amount
            to_account.balance += amount

Deadlock 是什麼

Deadlock 是「每個 thread 都在等別人釋放 lock,但沒有人能繼續」。

Thread A 持有 Lock 1,等待 Lock 2
Thread B 持有 Lock 2,等待 Lock 1

→ 永遠等下去

Coffman 條件(4 個都成立才會 deadlock)

條件說明
Mutual Exclusion資源只能被一個 thread 持有
Hold and Waitthread 持有資源的同時等待另一個
No Preemption資源不能被強制剝奪,只能自願釋放
Circular WaitA 等 B,B 等 C,C 等 A(形成環)

打破任何一個條件就能避免 deadlock。實務上最常用的是打破 Circular Wait:

# 壞的:Thread 1 拿 lock_a 再拿 lock_b,Thread 2 拿 lock_b 再拿 lock_a
# 好的:統一順序,所有人都先拿 lock_a 再拿 lock_b
 
# 或用 tryLock 加 timeout:拿不到就放棄,等一段時間再試
import threading
 
def transfer_safe(from_account, to_account, amount):
    # 用 id 決定 lock 的獲取順序,保證全局一致
    first = min(from_account.id, to_account.id)
    second = max(from_account.id, to_account.id)
    locks = {account.id: account.lock for account in [from_account, to_account]}
    
    with locks[first]:
        with locks[second]:
            if from_account.balance >= amount:
                from_account.balance -= amount
                to_account.balance += amount

怎麼 Debug Concurrent Bug

Race condition 的 debug 工具

  • Java-Djdk.internal.ref.UnsafeFieldUpdaterImpl + ThreadSanitizer(-fsanitize=thread
  • Go:內建 race detector,go test -racego run -race main.go
  • Pythonthreading.Lock + logging timestamp 手動追蹤(Python 有 GIL,純 Python code 不太會有 race condition,但 C extension 或 multiprocessing 才會)

Deadlock 的 debug 工具

  • JVMjstack <pid> 輸出所有 thread 的 stack trace 和 lock 持有狀態,搜尋 BLOCKEDdeadlock
  • Go:程式自動 panic 並印出所有 goroutine stack(all goroutines are asleep - deadlock!
  • Linuxstrace -p <pid> 看 system call 卡在哪

最有效的預防策略

  1. 盡量用不可變資料(immutable),沒有 mutation 就沒有 race
  2. 用 message passing 代替共享記憶體(Go channel、Erlang actor)
  3. 需要 lock 時,文件化 lock 的獲取順序並全局一致執行
  4. DB 層用 transaction + optimistic/pessimistic locking,不要自己用 application lock 管 DB 狀態

和 C03 Concurrency Patterns 的分工

本篇是概念層:理解 race condition 和 deadlock 是什麼,以及如何識別和預防。

C03 的 concurrency patterns(Thread Pool、Read-Write Lock、Producer-Consumer、Active Object、Reactor)是解法層:在理解概念後,用 pattern 把 concurrent 程式碼組織成可維護的結構。

理解本篇之後,C03 的 pattern 才有意義——Thread Pool 為什麼存在、Read-Write Lock 解了什麼問題、Producer-Consumer 的 buffer 為什麼能緩解 race。