# JavaScript中的Race Condition ## 前言 有時在後端或是作業系統會聽到Race Condition的狀況,簡單來說,就是至少兩個以上 Process 運行的中,同時讀寫相同的變數而導致錯誤發生的情況,當然以OS的角度來說,會有更嚴僅的定義,但本篇會以javascript 為例,介紹 race condition, 以理解何為 race condition 以及如何解決 race condition 所造成的問題。 ## 同步(synchronous)和異步(asynchronous) ### 簡介 在程式中,異步(asynchronous)代表著一個process獨立於其他 process運行,而同步(synchronous)代表著一個process僅在某個其他process完成或移交後而運行。可以把同步想像成只有一個人在做事,如果有五個不同的函式,同步的過程中,會按照順序執行,第一件事情做完,才能做第二件事情;反觀異步的過程中,可以想像有很多人同時進行,同樣的五個函式,異步的狀況,可以請五個人同時做這五件不同的事情,時間效率大大提升! ### JavaScript 是同步還是異步? 通常來說,JavaScript的特性是single-threaded synchronous,也就是說JS單打獨鬥是一次只會做一件事情的程式語言。當然,JS也有自己內建的異步函式(asynchronous function),例如常見的計時器setTimeout(),Promise以及async/await也是常見的異步函式。使用此函式的時候會另外開一個process且不影響原本process的執行 > 如果對於Promise型態不熟悉的人,請先參考這篇文章[什麼是Promise物件?](https://hackmd.io/@emmmmmma/r1CPXQhdp) ### 異步很棒啊? 確實使用異步函示可以在相同的時間做到更多的事情,但也有可能出現今天要討論的Race Condition狀況 舉例來說 小明是個經營一間家具行,然後收款方式都要以匯款為主,一張椅子賣500元,一張桌子賣1000元,並在程式中加入一個randomDelay模仿匯款時可能因為設備、網路等問題就算同時呼叫也會有些許的時間差異。 假設小明共收到四筆款項,依序為桌子、椅子、椅子、桌子,程式如下: ```javascript= let balance = 0 const randomDelay = () => { // return value is a Promise // and the time for this promise changing from pending to fulfilled // is random (0s-0.1s) return new Promise((resolve) => setTimeout(resolve, Math.random() * 100)); }; async function loadBalance() { await randomDelay(); // 等個隨機的0s~0.1s return balance; } async function saveBalance(value) { await randomDelay(); balance = value; } async function sellChair() { const balance = await loadBalance(); console.log(`賣椅子前,帳戶金額為: ${balance}`); const newBalance = balance + 500; await saveBalance(newBalance); console.log(`賣椅子後,帳戶金額為: ${newBalance}`); } async function sellTable() { const balance = await loadBalance(); console.log(`賣桌子前,帳戶金額為: ${balance}`); const newBalance = balance + 1000; await saveBalance(newBalance); console.log(`賣桌子後,帳戶金額為: ${newBalance}`); } // 收到四筆款項 async function main() { await Promise.all([ sellTable() sellChair() sellChair() sellTable() ]); balance = await loadBalance(); console.log(`賣椅子與桌子後的帳戶金額是$${balance}`); } main(); ``` 執行程式後就會發現,每次執行但金額卻不太一樣,這就是發生了race condition,因為兩個異步的函式sellTable和sellChair中,從loadBalance到saveBalance的過程中,呼叫了相同的變數balance,導致沒有存到正確的數值,且不僅不同買賣的順序被打亂,每次的結果還會不太一樣!  ## Mutex(互斥鎖) 為了解決這個問題我們就需要確保放問共享資源(如上面的balance)時,先把第一筆匯款的帳戶餘額上鎖,上鎖期間其他的Thread都無法訪問這個資源,等待他被執行完,再解鎖來執行第二個匯款 所以我們可以透過Mutex來檢查是否可以進入,透過設定let mutex = Promise.resolve() 確保回傳的是一個fulfill的狀態再繼續去執行,所以每次進入mutex.then()內部的callback function能否被執行。只有當前一個promise進入fulfilled,程式才會繼續執行,以避免發生race condition的狀況! ```javascript= let balance = 0 let mutex = Promise.resolve(); // return fulfilled Promise object const randomDelay = () => { return new Promise((resolve) => setTimeout(resolve, Math.random() * 100)); }; async function loadBalance() { await randomDelay(); // 等個隨機的0s~0.1s return balance; } async function saveBalance(value) { await randomDelay(); balance = value; } async function sellGrapes() { mutex = mutex .then(async () => { const balance = await loadBalance(); console.log(`賣椅子前,帳戶金額為: ${balance}`); const newBalance = balance + 500; await saveBalance(newBalance); console.log(`賣椅子後,帳戶金額為: ${newBalance}`); }) .catch((e) => { console.log(e); }); return mutex; } async function sellOlives() { mutex = mutex .then(async () => { const balance = await loadBalance(); console.log(`賣桌子前,帳戶金額為: ${balance}`); const newBalance = balance + 1000; await saveBalance(newBalance); console.log(`賣桌子後,帳戶金額為: ${newBalance}`); }) .catch((e) => { console.log(e); }); return mutex; } async function main() { await Promise.all([ sellOlives(), sellGrapes(), sellGrapes(), sellOlives(), ]); const balance = await loadBalance(); console.log(`賣椅子與桌子後的帳戶金額是$${balance}`); } main(); ``` 透過上面的程式碼就會發現程式這次可以成功執行,且金額和順序都正常!  ## 總結 使用Ajax或是呼叫api修改資料庫的時候,常常會遇到race condition的狀況,讓人覺得明明邏輯對但是為什麼結果就是不如預期,後端資料庫也可能遇到類似的情況,例如訂票的系統,兩個人同時下單買票,如果沒有確實lock的話,就有可能發生這兩個人都買到同一個位置票的情況,結果就是會有消費者會氣噗噗的去和公司吵架,希望透過這篇文章可以讓大家對於js更加熟悉,寫程式都可以快速找到bug! ## 後記 要去解決race condition的方式很多,如果不是上述單純前端是牽扯到呼叫api的狀況,也可以參考這篇文章[避免 AJAX 造成的 Race condition](https://uu9924079.medium.com/%E9%81%BF%E5%85%8D-ajax-%E9%80%A0%E6%88%90%E7%9A%84-race-condition-9db32b8415a4)裡面有用AbortController的方式去避免race condition的情況發生。此外也可以在更底層的資料庫中就先設定好check-and-set的機制來避免錯誤,可以參考[RDBMS - Race Condition 介紹及解決思路](https://blog.kennycoder.io/2020/02/07/RDBMS-Race-Condition%E4%BB%8B%E7%B4%B9%E5%8F%8A%E8%A7%A3%E6%B1%BA%E6%80%9D%E8%B7%AF/)
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up