併發設計⚡談談鎖與資源

Jayden Lin
程式猿吃香蕉
Published in
5 min readSep 12, 2022

筆者曾任職 Yahoo,現在區塊鏈產業打滾,《軟體需求溝通 ─ 從外商公司學跨部門協作開發》線上課程講師,紛絲團《程式猿吃香蕉🍌

在寫併發程式的時候,「鎖」和「資源」是兩個不同的東西,如果沒有弄清楚,有時會出現鎖不住,或是鎖錯資源的情況。

以生活化的例子來說,鎖和資源它們倆就像是鑰匙跟抽屜 (含內容物) 的對應關係。

有時候一個鑰匙可以對應多個抽屜,有時候一個鑰匙只能開一個抽屜,或者是多個鑰匙開很多層才可以開一個抽屜。雖然概念上看似很簡單,但在實際的架構中或是程式語法裡面不一定很容易地看出來。身為工程師,當然是寫Code 表達最清晰,接下來我用幾段簡單的 Java 程式碼來呈現這個概念

▍找找看鎖在哪裡?

舉個例子:下面的 Java 程式碼定義了 TV 這個類別,同時有 watchMovie 跟 playVideoGame 兩個方法。並且在 watchMovie 和 playVideoGame 加上 synchronized 修飾字,目的是為了讓一台 TV 不能同時執行 watchMovie 以及 playVideoGame

這段程式碼,你看得出來那個部分是「資源」而哪個部分表示「鎖」嗎?(想想鑰匙和抽屜的例子,哪裡是抽屜?哪裡是鑰匙?)

可能有些讀者看出來了,在我用藍字註解的地方寫著資源 A 和資源 B,分別表示 watch movie 以及 play video game 這兩件事。 watch movie 以及 play video game 這兩件事就是「資源」的部分。為了避免 資源 A 和資源 B 被同時執行,我們需要一把鎖,那麼「鎖」的位置又在哪呢? 我們將上述的程式碼轉換成下面的格式會更好理解:

我們將 synchronized 從方法上移除,改為在方法內建立一個 synchronized 的區塊,這兩種寫法意義上是一樣的。而 synchronized 括號內的 this ,就表示進入這個區塊前,要先取得的「鎖」。其中,Java 的 this 是TV 類別實例化後的值,也就是說資源 A 和資源 B 被同一把鎖 (就是 this) 給鎖住,以這種方式避免資源 A 和資源 B 被同時執行。鎖的位置跟資源的位置,在上圖中我用藍色註解清楚地標示了出來。

我們還可以改寫一下程式,更明顯地把「鎖」呈現出來,下圖我用一個變數 lock 當作鎖,來鎖住資源 A 和資源 B:

接下來我們來看執行的情況 (如下圖),當兩個執行緒 t1 和 t2 同時執行的時候,因為 new TV() 時在類別內部只會產生一個 lock 實例,所以只有一把鎖,可以避免 t1 跟 t2 兩個執行緒同時獲得鎖,達成同一時間只有一個資源被執行的效果。

▍鎖和資源誤用的情況

在明白了鎖跟資源之間的獨立關係後,還需要仔細的考慮鎖和資源的對應問題:

假設剛才的程式,我們一不小心寫成 new TV() 兩次 (如下圖),產生了 tv1 和 tv2 ,這時候 tv1 內部有自己的 lock 實例,而 tv2 內部也有自己的 lock 實例,各自的鎖用來鎖自己的資源。兩個 TV 實例可以各自執行 watch movie 以及 play video game,讓 watch movie 以及 play video game 事件同時發生,但這並不一定是你想要的結果。

▍小結

從上面的例子我們可以很清楚地知道:

在併發的程式裡面,鎖是鎖,資源是資源,在系統設計時需要把兩者分開思考。

像我們剛才舉的例子,要在程式中找出隱晦的鎖與資源對應關係 (例如:synchronized語法),如果不小心弄出了兩把鎖 (new TV兩次產生兩個 lock),可能會出現不希望發生的情況。

這個鎖與資源的觀念是具體而微的,在微觀上,一小段 Java 程式是這樣呈現,在宏觀上更複雜的系統架構,或甚至是分散式的環境下,也可以套用同樣的道理。鎖會有更多不同的屬性 (共享鎖、排他鎖等等) 也會有不同的策略(悲觀鎖、樂觀鎖等等),需要考慮鎖之間兼容的問題,而對資源的操作也會更複雜,之後我會再分享更多的例子。但總體來說,概念是不變的,在併發程式中,我們應該細心地去分辨當下的鎖在哪裡以及資源在哪裡,並且小心翼翼的設計鎖和資源的對應

若是喜歡我分享的內容,可以訂閱我的粉絲團《程式猿吃香蕉🍌,在軟體開發的路上,一起分享交流,一起成長。

--

--

Jayden Lin
程式猿吃香蕉

曾在 Yahoo 擔任 Lead Engineer,負責廣告系統,帶團隊做跨國開發,現任職區塊鏈產業。也是《程式猿吃香蕉》團隊創辦人,喜歡將實用的軟體知識以簡單生動的方式講給大家聽 😄😄😄