Concurrent & Racing Conditions (並發與競態條件)
最近因為我在開發 Zoraxy 時需要設計一個可以做到 high speed concurrent READ 跟間中 write 的 map,跟另一位高雄的工程師大佬討論了一個晚上,後來我發現原來滿多人對 concurrent / racing conditions 的處理有一定程度的誤解,所以我就來簡單的解釋一下最常見的誤解 - Check for locking。 問題 假設你有兩個 process,一邊要讀取一個無法同時讀寫的 map 資料結構(例如 Go 的 map[string]struct{}),另一個要寫入新的 key 到同一個 map。由於 map 並沒辦法處理 concurrent R/W,所以你要怎樣做呢? 解決方法 1:改用專用的 concurrent map (如 Go 的 sync.Map) 這個解決方法簡單直接,我就不再詳細解釋了。然而由於我在開發 Zoraxy 的 high concurrent access 特質,使用 sync.Map 的話會做成很高的 over-head,因此我並不是很想用它(但是也不影響 sync.Map 作為沒法解決時用的最後方案的決定) 解決方法 2:mutex lock 這算是比較正規的方案之一。在讀取和寫入之前先對 map (或是 slice) 進行鎖定,然後在寫入或讀取之後解鎖。這樣就能確保同時間只有一個 process 對這個 map 有 R/W access。但是這同時也會產生另一個問題,就是在 read lock 了之後,假設有另一個 process 也想同時進行讀取的話就必須要等前面的讀完,因此這個方案對於高並發的 request handling 來說並不是太有效率。 那聽到這裡的時候很多人會直覺的覺得:那只有 write 的時候 lock 就好啦,read 的時候檢查 map 是不是被 lock,沒 lock 的話就讀取。由於檢查的時候不會 lock,因此其他需要 read 的 process 也不會被擋不是嗎? 聽上去好像沒錯喔?讓我簡單寫一個 pseudocode 例子: function write(){ mutex.lock() map.write("foo", "bar") mutex.unlock() } function read(){ if (!mutex.isLocked()){ //Map is not locked var value = map.read("foo") print(value) }else{ //Map is locked, retry after 100ms delay(100) read() } } mutex 的部分可以是 programming language 自帶的 mutex 功能或是簡單的 boolean,效果都是一樣的。但是如果你有注意到,你不會找到 mutex.isLocked() 這個功能。 如果你需要用到這個,很大機會你寫錯了 這是為甚麼呢?那是因為 OS 層基本運作原理所引起的。這裡對於本科沒修 Computer Architecture (計算機架構)的工程師可能比較難理解。簡單來說就是,CPU 上就只有 n 個核心,但是你的作業系統上面有很多東西在執行,所以 OS 會對上面執行的工作(process)進行快速切換。每次切換的時候因為速度很快,所以會讓你有一種它們在並行處理的感覺。而實質上,每個 process switching 都會有一個 overhead,而這個 overhead 就是為甚麼 mutex lock 沒辦法被用於檢查的原因。 其中一種常用的 scheduling 演算法:Round Robin Process Switching 關於 CPU scheduling 跟 scheduling 演算法的詳情可以看這邊 所以為甚麼 Mutex 不能檢查是不是被其他 process 鎖上? 當你去檢查一個 mutex 是否被鎖的時候,檢查當下可能沒被鎖,但是檢查完到執行下一個邏輯的時候可能已經被另一個 process 鎖上了…