Go 為什麼不支援可重入鎖?

煎魚發表於2021-12-21

大家好,我是煎魚。

程式裡的鎖,是很多小夥伴在寫分散式應用時用的最多的一個利器之一。

使用 Go 的同學裡,絕大部分都有其他語言的經驗,就會對其中一點有疑惑,那就是 Go 裡的鎖,竟然不支援可重入

為此,今天煎魚帶大家一起來了解這裡的設計考量,看看為什麼。

可重入鎖

如果對已經上鎖的普通互斥鎖進行 “加鎖” 操作,其結果要麼失敗,要麼會阻塞至解鎖。

鎖的場景如下:

  • 在加鎖上:如果是可重入互斥鎖,當前嘗試加鎖的執行緒如果就是持有該鎖的執行緒時,加鎖操作就會成功。
  • 在解鎖上:可重入互斥鎖一般都會記錄被加鎖的次數,只有執行相同次數的解鎖操作才會真正解鎖。

簡單來講,可重入互斥鎖是互斥鎖的一種,同一執行緒對其多次加鎖不會產生死鎖,又或是導致阻塞。

不同語言間實現可能或多或少有些區別,但大體意思差不多。

請你想一下,Go 是怎麼樣的呢?

Go 支援情況

我們看到以下這個 Go 互斥鎖例子:

var mu sync.Mutex

func main() {
    mu.Lock()
    mu.Lock()
}

這段 Go 程式會阻塞嗎?不會,會報以下錯誤:

fatal error: all goroutines are asleep - deadlock!

Go 顯然是不支援可重入互斥鎖的。

官方回覆

Go 設計原則

在工程中使用互斥的根本原因是:為了保護不變數,也可以用於保護內、外部的不變數。

基於此,Go 在互斥鎖設計上會遵守這幾個原則。如下:

  • 在呼叫 mutex.Lock 方法時,要保證這些變數的不變性保持,不會在後續的過程中被破壞。
  • 在呼叫 mu.Unlock 方法時,要保證:

    • 程式不再需要依賴那些不變數。
    • 如果程式在互斥鎖加鎖期間破壞了它們,則需要確保已經恢復了它們。

不支援的原因

講了 Go 自己的設計原則後,那為什麼不支援可重入呢?

其實 Russ Cox 於 2010 年在《Experimenting with GO》就給出了答覆,認為遞迴(又稱:重入)互斥是個壞主意,這個設計並不好。

我們可以結合官方的例子來理解。

如下:

func F() {
        mu.Lock()
        ... do some stuff ...
        G()
        ... do some more stuff ...
        mu.Unlock()
}

func G() {
        mu.Lock()
        ... do some stuff ...
        mu.Unlock()
}

在上述程式碼中,我們在 F 方法中呼叫 mu.Lock 方法加上了鎖。如果支援可重入鎖,接著就會進入到 G 方法中。

此時就會有一個致命的問題,你不知道 FG 方法加鎖後是不是做了什麼事情,從而導致破壞了不變數,畢竟隨手起幾個協程做點壞事,也是完全可能的。

這對於 Go 是無法接受的,可重入的設計違反了前面所提到的設計理念,也就是:“要保證這些變數的不變性保持,不會在後續的過程中被破壞”。

基於上述原因,Go 官方團隊選擇了沒有支援該項特性。

總結

Go 互斥鎖沒有支援可重入鎖的設計,也是喜歡的大道至簡的思路了,可能的干擾比較多,不如直接簡單的來。

你在工作過程中有沒有類似的疑惑呢,歡迎大家在評論區留言和交流:)

若有任何疑問歡迎評論區反饋和交流,最好的關係是互相成就,各位的點贊就是煎魚創作的最大動力,感謝支援。

文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。

相關文章