04-1. Thread Safety 基礎概念

⏱️ 閱讀時間: 10 分鐘 🎯 難度: ⭐⭐ (簡單)


🤔 什麼是 Thread Safety?

一句話解釋: Thread Safety(線程安全)是指多個 Thread 同時存取共享資源時,程式仍能正確執行,不會產生錯誤結果。


🏢 用銀行帳戶來比喻

不安全的情況

銀行帳戶:餘額 = 1000 元

時間 T0:
├─ 員工 A(Thread A): 讀取餘額 = 1000
└─ 員工 B(Thread B): 讀取餘額 = 1000

時間 T1:
├─ 員工 A: 存入 100 → 計算 1000 + 100 = 1100
└─ 員工 B: 提取 50 → 計算 1000 - 50 = 950

時間 T2:
├─ 員工 A: 寫入 1100
└─ 員工 B: 寫入 950

最終餘額: 950  ← 錯誤!應該是 1050

這就是 Thread Safety 問題!


🔍 實際案例

案例 1:計數器問題

from threading import Thread

counter = 0

def increment():
    global counter
    for _ in range(100000):
        counter += 1  # 這不是原子操作!

# 創建兩個 Thread
t1 = Thread(target=increment)
t2 = Thread(target=increment)

t1.start()
t2.start()
t1.join()
t2.join()

print(f"Counter: {counter}")
# 預期: 200000
# 實際: 可能是 150000 或其他值 ← 不安全!

為什麼會錯誤?

# counter += 1 實際上是三個步驟:
1. temp = counter      # 讀取
2. temp = temp + 1     # 計算
3. counter = temp      # 寫入

# 兩個 Thread 可能交錯執行:
Thread A: temp_a = counter  (100)
Thread B: temp_b = counter  (100)   還是 100
Thread A: temp_a = 101
Thread B: temp_b = 101
Thread A: counter = 101
Thread B: counter = 101   覆蓋了 A 的結果

案例 2:使用 Lock 解決

from threading import Thread, Lock

counter = 0
lock = Lock()

def safe_increment():
    global counter
    for _ in range(100000):
        with lock:  # 加鎖
            counter += 1

t1 = Thread(target=safe_increment)
t2 = Thread(target=safe_increment)

t1.start()
t2.start()
t1.join()
t2.join()

print(f"Counter: {counter}")
# 輸出: 200000 ← 正確!

Lock 的作用:

Thread A 獲得 Lock:
  counter += 1  ← 只有 A 可以執行
Thread A 釋放 Lock

Thread B 獲得 Lock:
  counter += 1  ← 現在 B 可以執行
Thread B 釋放 Lock

確保同一時間只有一個 Thread 修改 counter!

📋 Thread Safety 的三大問題

1. Race Condition(競態條件)

定義: 多個 Thread 競爭存取共享資源,結果取決於執行順序。

# ❌ 不安全
balance = 1000

def withdraw(amount):
    global balance
    if balance >= amount:  # 檢查
        # 其他 Thread 可能在這裡插入!
        balance -= amount  # 提取

# 兩個 Thread 同時提取 600
# 可能都通過檢查,導致餘額變負數!

2. Deadlock(死鎖)

定義: 多個 Thread 互相等待對方釋放資源,導致永遠阻塞。

from threading import Lock

lock_a = Lock()
lock_b = Lock()

def task1():
    with lock_a:
        print("Task1 獲得 Lock A")
        with lock_b:  # 等待 Lock B
            print("Task1 獲得 Lock B")

def task2():
    with lock_b:
        print("Task2 獲得 Lock B")
        with lock_a:  # 等待 Lock A
            print("Task2 獲得 Lock A")

# Task1 持有 A,等待 B
# Task2 持有 B,等待 A
# 互相等待 → Deadlock!

3. Data Race(資料競爭)

定義: 多個 Thread 同時存取同一記憶體位址,且至少有一個是寫入操作。

# ❌ Data Race
shared_data = []

def writer():
    shared_data.append(1)  # 寫入

def reader():
    if len(shared_data) > 0:  # 讀取
        print(shared_data[0])

# Writer 和 Reader 同時執行
# 可能讀到不一致的狀態!

🔧 確保 Thread Safety 的方法

1. Lock(互斥鎖)

from threading import Lock

lock = Lock()

def critical_section():
    with lock:
        # 臨界區:同一時間只有一個 Thread
        shared_resource.modify()

2. RLock(可重入鎖)

from threading import RLock

rlock = RLock()

def recursive_function(n):
    with rlock:
        if n > 0:
            recursive_function(n - 1)  # 可以重複獲得鎖

3. Semaphore(信號量)

from threading import Semaphore

# 最多允許 3 個 Thread 同時執行
semaphore = Semaphore(3)

def limited_access():
    with semaphore:
        # 最多 3 個 Thread 可以同時進入
        use_resource()

4. Queue(線程安全佇列)

from queue import Queue

# Queue 內建 Thread Safety
q = Queue()

def producer():
    q.put(data)  # 安全

def consumer():
    data = q.get()  # 安全

🎯 Thread Safety 檢查清單

設計階段

  • 識別所有共享變數
  • 確定哪些操作需要同步
  • 選擇合適的同步機制

實作階段

  • 使用 Lock 保護臨界區
  • 避免嵌套鎖(防止 Deadlock)
  • 使用線程安全的資料結構

測試階段

  • 壓力測試(大量並發)
  • 檢查 Race Condition
  • 監控 Deadlock

✅ 重點回顧

Thread Safety 的定義:

  • 多 Thread 同時存取共享資源時,程式仍能正確執行

三大問題:

  1. Race Condition:執行順序影響結果
  2. Deadlock:互相等待資源
  3. Data Race:同時讀寫同一記憶體

解決方法:

  • ✅ Lock(互斥鎖)
  • ✅ RLock(可重入鎖)
  • ✅ Semaphore(信號量)
  • ✅ 使用線程安全的資料結構

關鍵理解:

  • ✅ 共享記憶體是問題來源
  • ✅ 同步機制確保安全
  • ✅ 過度同步影響效能
  • ✅ 設計階段就要考慮 Thread Safety

下一篇: 04-2. Race Condition(競態條件)


最後更新:2025-01-04

0%