面試時,每當談到平行處理(parallel processing)、多執行緒(multithreading)、多行程(multi-processing)之後,通常都會衍生一些關於 race condition 的問題,也可稱為 race hazard(競爭危害)。
本文以實際 Python 範例創造 race condition, 以理解何為 race condition 以及如何解決 race condition 所造成的問題。
Race condition / Race hazard
以下是 1 個經典的 Race condition 例子,當 John 與 Alice 同時 讀到 同一筆 John 銀行餘額 $100 之後,他們各自加了 $10 與 $5 之後,更新回 John 銀行餘額。
這導致 John 存款之後,發現他帳戶餘額只有 $105,不是預期的 $115 ($100 + $10 + $5);而對銀行來說也莫名少了 $10 。
所以 Race condition 的存在要件,通常都是以下 2 點:
- 平行處理
- 對同一個資源同時進行存取操作
下列 Python 程式碼,用 threading 模組模擬對 2 個人同時對 John 銀行金額進行各 100000 次 +1 與 -1 操作的結果,其正確答案為 0 ,但會因為 race condition 導致有時候不是 0 :
import threading
JOHN_TOTAL = 0
def add(num):
global JOHN_TOTAL
for _ in range(100000):
JOHN_TOTAL += num
while JOHN_TOTAL == 0:
john = threading.Thread(target=add, args=(1, ))
alice = threading.Thread(target=add, args=(-1, ))
threads = (john, alice, )
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(f"John's total: {JOHN_TOTAL}")
上述範例執行結果,如下圖所示:
p.s. 上述範例用 while 迴圈不斷嘗試,直到 race condition 出現為止,可能要試一陣子才會出現,建議可以貼到Google Colab 執行看看,較容易試出 race condition
理解 race condition 之後,接下來就可以嘗試理解何謂鎖(Lock)。
Race condition 的解決方法 - Lock
解決 race condition 的辦法也很簡單,就是上鎖(Lock)。只有允許取得 Lock 的 thread/process 能夠對某一資源進行存取操作,才能徹底解決 race condition 的問題。
以先前章節提到的例子來說,只要針對 John 銀行存款值上鎖,讓同一時間只有 1 個 thread/process 能夠存取與更新操作,就能避免 race condition 所造成的不正確金額問題。
下列範例為加上 lock 之後的程式碼,可以發現每個 thread 執行 add 函式時,必須先執行 lock.acquire()
獲得 lock, 而更新操作完之後必須執行 lock.release()
釋放 lock ,讓其他 thread 有機會獲得 lock 執行工作:
import threading
JOHN_TOTAL = 0
LOCK = threading.Lock()
def add(lock, num):
lock.acquire()
global JOHN_TOTAL
for _ in range(100000):
JOHN_TOTAL += num
lock.release()
RETRY_COUNT = 0
while JOHN_TOTAL == 0 and RETRY_COUNT < 10:
john = threading.Thread(target=add, args=(LOCK, 1, ))
alice = threading.Thread(target=add, args=(LOCK, -1, ))
threads = (john, alice, )
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print(f"John's total: {JOHN_TOTAL}")
RETRY_COUNT += 1
不過,加上 Lock 之後,程式的執行速度就會變慢,因為每個 thread 都必須花費時間等待直到獲得鎖為止;另外,如果有程式上的 bug 或是邏輯問題導致 lock 遲遲未被釋放,也會導致整個程式卡住,所以使用 lock 也必須注意程式在例外情況下是否能夠正確釋放 lock 。
總結
Race condition 不單單只存在於多 thread / 多 process 的情況,只要符合以下 2 點,就可能發生:
- 平行處理
- 對同一個資源同時進行存取操作
例如多個使用者同時上傳資料到同一個路徑,或者多個使用者同時編輯同一個文件,又或者多個 API 同時儲存某一筆快取,多個 API 同時更新某一筆資料庫記錄(record)都可能發生 race condition, 因此在設計系統、程式架構時,一旦提到平行處理,都勢必得額外注意是否會發生 race condition 的問題。
References
https://docs.python.org/3/library/threading.html
https://en.wikipedia.org/wiki/Race_condition