在程式設計中,平行處理可能會同時訪問共享資源,這樣就可能導致共享資源的競爭與執行結果不一致等等問題。
為了避免這些問題,就需要使用 lock 進行同步,保證在同一時間只有一個執行緒或 process 能夠存取共享資源。
所以在面試中,許多面試官都會考面試者對於 lock 的了解和應用。常見的考題包括 lock 的原理、種類、常見問題和解決方法等。
了解 lock 的使用不僅僅可以應對相關面試問題,也有助於在實際開發中知道如何使用 lock, 以避免平行處理所造成的相關問題。
關鍵區段(Critical section)與共享資源(Shared resource)
在談 Lock 之前,一定要了解何謂 critical section 與 shared resource 。
Critical section 或稱為關鍵區段,指的是一個存取共享資源(例如資料庫中的資料)的程式片段,例如更新使用者存款的程式碼片段,就稱為 critical section ,而使用者存款就是一個共享資源,而這些共享資源有著無法同時被多個執行緒或者 process 存取的特性,同樣舉存款為例,多個執行緒或 process 同時存取存款就可能因為 race condition 導致金額更新之後不正確,詳見 後端工程師面試考什麼 - Race Condition 篇 。
有了 critical section 與 shared resource 的概念之後,在寫程式的當下就會知道哪邊要上 lock 才安全。
常見的 lock 們
Mutex Lock (互斥鎖)
Mutex lock 是一種常見的 lock, 應用於控制對共享資源的存取。
當一個執行緒或 process A 想要存取一個共享資源時,它必須先獲得 Mutex Lock, 如果另一個執行緒或 process B 已經獲得了 mutex lock, 那麼 A 必須等待 B 釋放該 Lock,才能繼續進行。
通常程式語言都會內建 mutex lock 相關的函式以實現同步(synchronization)。
應用場景
如果你需要保證同一時間只有 1 個執行緒或 process 可以存取共享資源, 那麼使用 mutex lock 準沒錯。
Read-Write Lock (讀寫鎖)
Read-Write Lock 也是一種常見的 lock,不過它區分讀取和寫入 2 種操作。
當一個執行緒或 process 想要進行讀取操作時,它可以獲得 read lock, 這樣其他執行緒或 process 也可以繼續進行讀取操作,但不能進行寫入操作。
當一個執行緒或 process 想要進行寫入操作時,它必須獲得 write lock, 這樣其他執行緒或 process 都不能進行讀取或寫入操作。
也由於 Read-Write lock 區分為讀取和寫入 2 種操作,所以實作上也相對較 mutex lock 更為複雜,比較直覺的方式是分別實作一個 read 與 write 的計數器,當 read 計數器不為 0 時,不能進行 write 操作,但是可以進行 read 操作,並且 read 計數器加 1 ,代表正在進行讀取的數量,讀取結束則 read 計數器扣 1 ;反之,當 write 計數器不為 0 時,則不可以進行 read 操作,而且 write 操作必須等 read 計數器為 0 時才能進行。
由於 write 操作必須等 read 計數器為 0 時才能進行,所以很有可能 write 的操作會等非常久甚至等不到,而導致 write 操作遲遲無法進行,因此在實作上也必須考慮 write 操作有較高的優先權,或者 read 優先權較高,或者 2 者沒有任何優先權的保證。
應用場景
Read-Write Lock 可以用於優化對共享資源的存取,例如在多執行緒或多 process 應用程式中,讀取的操作大於寫入的操作時,使用讀寫鎖可以提高應用程式的效率,雖然 write lock 的等待還是省不了,但因為讀取的時候不用等待其他執行緒或 process 釋放 read lock, 因此而提升效率。
Semaphore
Semaphore 可以控制對共享資源的存取數量,譬如設定 Semaphore 為 5 時,就代表同一時間僅有 5 個執行緒或者 process 可以存取共享資源。
它可以被看作是有設定初始值的計數器,以 Semaphore(5)
為例,就代表計數器初始值為 5, 當一個執行緒或 process 想要存取 1 個共享資源時,它必須先獲得 Semaphore, Semaphore 會視現在計數是否大於數值 0 ,如果是,則讓該執行緒獲得 Semaphore, 並且扣掉計數值(-1)以控管存取共享資源的數量,否則該執行緒或 process 則必須等待其他執行緒或 process 釋放 Semaphore 讓計數值大於 0 以獲得 Semaphore 才能繼續存取共享資源。
應用場景
Semaphore 可以用於控制對共享資源的存取,例如資料庫的 connection pool 可以使用 Semaphore 進行實作,藉此控制資料庫的連線數量,也避免多個執行緒或 process 共用同 1 個連線。
使用 Lock 的 2 大常見問題
死結(Deadlock)
死結是指當兩個或多個 process 或執行緒在互相等待對方釋放資源,導致彼此都無法繼續執行的一種狀態。通常 deadlock 會導致系統停擺,造成嚴重的問題。
例如,當 process A 擁有資源 X,但需要資源 Y 才能繼續執行,而process B 擁有資源 Y,但需要資源 X 才能繼續執行,這時就會形成死結。
打破死結的解決方法有很多,例如避免同時持有多個 lock 過久、規定按一定順序獲得 lock 等等,不過實務上死結發生的時機點通常都很隨機,也不太好發現是死結,因此在一開始針對這種必須同時持有多個 lock 的程式就必須特別注意是否會有機率發生死結。
飢餓(Starvation)
Starvation 是指當多個 process 或執行緒競爭同一個鎖時,某些 process 或執行緒一直無法取得鎖,而長時間地停擺,從而無法繼續執行的狀況。
例如 Read-Write lock 提到的 write lock 一直因為 read 計數器遲遲無法歸零所造成的 write 操作一直等待的情況,就是一種 starvation 。
解決 starvation,可以使用一些策略,如導入公平性(fairness)、自旋鎖(spin lock)、時間限制等等。公平性(fairness)可以用排隊的方式給予每個執行緒或 process 機會輪流取得 lock, 是一種能夠確保每個執行緒或 process 都能夠平等取得 lock 的機制,自旋鎖則是一種透過快速反覆嘗試來獲得 lock 的方法,最直覺的方法就是使用 while loop 一直嘗試獲得 lock, 能夠避免某些執行緒或 process 長時間佔用 lock 。
總結
不同的 lock 有不同的使用場景,我們需要根據功能的特點和需求選擇適合的 lock 來保護共享資源,實現對共享資源的同步和保護,提高應用程式的效率和穩定性。
References
https://zh.wikipedia.org/zh-tw/%E8%87%A8%E7%95%8C%E5%8D%80%E6%AE%B5
https://en.wikipedia.org/wiki/Lock_(computer_science)
https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock