後端工程師面試考什麼 - Race Condition 篇

Posted on  Oct 17, 2022  in  後端面試準備  by  Amo Chen  ‐ 3 min read

面試時,每當談到平行處理(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 點:

  1. 平行處理
  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 點,就可能發生:

  1. 平行處理
  2. 對同一個資源同時進行存取操作

例如多個使用者同時上傳資料到同一個路徑,或者多個使用者同時編輯同一個文件,又或者多個 API 同時儲存某一筆快取,多個 API 同時更新某一筆資料庫記錄(record)都可能發生 race condition, 因此在設計系統、程式架構時,一旦提到平行處理,都勢必得額外注意是否會發生 race condition 的問題。

References

https://docs.python.org/3/library/threading.html

https://en.wikipedia.org/wiki/Race_condition

FOLLOW US

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!