Go Mutex 之4種易錯場景盤點

2021-03-02 碼農的自由之路

考慮到各種原因,以後技術類的文章暫定每周四發布。

Mutex系列是根據我對晁嶽攀老師的《Go 並發編程實戰課》的吸收和理解整理而成,如有偏差,歡迎指正~

把 Mutex 設計原理看完之後,再看這一章,就簡單很多了。Mutex 的實現比較複雜,但是使用起來還是非常簡單的,它只有 Lock/Unlock 兩個方法。

但是很多時候,由於疏忽,或者因為對代碼的上下文不了解,就會出現一些bug。這一篇中,我們一起來看一下使用 Mutex 常犯的幾個錯誤。

Lock和Unlock 不成對出現

Lock/Unlock 操作不成對出現,有以下兩種情況:

1)只有 Lock 的情況

如果有一個協程只進行了 Lock 操作,沒有進行 Unlock 操作,那麼其它協程就會一直被阻塞(拿不到鎖),整個系統隨著阻塞的協程越來越多,最終崩潰。

2)只有 Unlock 的情況

如果一個協程只進行了 Unlock 操作,會出現兩種情況:

如果有其它協程佔有鎖,鎖會被釋放

如果鎖處於未被佔有狀態,會拋 panic

前面提過,Mutex 不記錄協程的信息,所以不管是哪個協程執行 Unlock 操作,都會產生解鎖的效果。

大家可能會覺得這麼簡單的錯誤,怎麼可能會出現呢?但是,後面會介紹到,哪怕是大型知名的開源項目,也會犯這些錯誤。

Copy 已使用的 Mutex

先介紹一個知識點:sync 包中的同步原語是不能複製的,Mutex 作為最常用的同步原語,當然也不能複製。

原因在於 Mutex 是一個有狀態的對象。如果複製一個 Mutex,會將相應的狀態也複製過來。這樣一個新的 Mutex 可能莫名處於持有鎖、喚醒或者飢餓狀態,甚至等阻塞等待數量遠遠大於0。

不過 Go 有死鎖檢查機制,如果出現了這種情況,直接運行,是會報錯的。除此以外,還可以用 go vet 工具主動檢查死鎖。

示例:

package main
import "sync"
func copyF(m sync.Mutex) { m.Lock() defer m.Unlock()
return}
func main() { var m sync.Mutex m.Lock() defer m.Unlock()

copyF(m) return}

直接 go run 會報錯,通過 go vet main.go,結果如下:

main.go:11:14: copyF passes lock by value: sync.Mutexmain.go:26:8: call of copyF copies lock value: sync.Mutex

如果只是單純的複製 Mutex,不使用的話,go run 可以正常運行,只有 go vet 能檢測出來。

重入

先解釋一下什麼是」可重入鎖「。我們都知道,一個協程搶佔鎖之後,其它協程無法搶佔鎖,但是如果該協程自己再次加鎖呢?可重入鎖是指當前協程還能再次搶佔鎖。可重入鎖一般也叫做」遞歸鎖「

Mutex 不是可重入鎖! 前面提過,Mutex 不會記錄持有鎖的協程的信息,所以它也無法區分是不是重入這種場景。

既然 Mutex 因為不記錄持有鎖的協程信息,我們怎麼讓 Mutex 具備重入的特性呢?答案就是想辦法讓 Mutex 帶上協程標誌信息。。

方案有兩種:

獲取 goroutine id,用這個 id 來唯一標識協程。這個有開源的包能幹這個事情。

協程自己提供 token,用這個 token 來唯一標識協程。

這兩種方案思路都是獲取協程的唯一標識,這樣重入加鎖的時候,才能判斷是不是同一個協程在加鎖。

type RecursiveMutex struct {    sync.Mutex    owner     int64     recursion int32 }
func (m *RecursiveMutex) Lock() { gid := goid.Get() if atomic.LoadInt64(&m.owner) == gid { m.recursion++ return } m.Mutex.Lock() atomic.StoreInt64(&m.owner, gid) m.recursion = 1}
func (m *RecursiveMutex) Unlock() { gid := goid.Get() if atomic.LoadInt64(&m.owner) != gid { panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid)) } m.recursion-- if m.recursion != 0 { return } atomic.StoreInt64(&m.owner, -1) m.Mutex.Unlock()}


其實這一段代碼只要看2-6行的 RecursiveMutex 的定義就能猜出來大概的邏輯:標誌鎖的持有者,並用一個計數器來記錄重入的次數。重入的時候,實際操作就是計數器+1,解鎖的時候計數器-1,直到最後一次,真正釋放鎖。

死鎖

死鎖就是兩個及兩個以上的協程在執行過程中,因為爭搶資源而處於一種相互等待的情況。如果沒有外部幹涉,它們都將無法推進下去,這種情況就稱之為死鎖。

死鎖產生的必要條件

互斥:被爭搶的資源是排他不共享的

持有和等待:某協程持有一個資源,並且還在請求其它協程持有的資源。

不可剝奪:資源只能等持有它的協程釋放

環路等待:n個協程P1-Pn,P1等待P2,...,Pn-1等待Pn,最後Pn等待P1

如果要避免產生死鎖,只要破壞上面4個條件中的1個或多個就行。

其實前面 Copy 已使用的 Mutex 的示例就符合上面的4個條件(把 main 函數和 copyF 想像成兩個協程)。尤其是最後一個條件,main 函數得等 copyF 完成才能進行下一步;而 copyF 又因為拿不到鎖(等 main 釋放鎖),就阻塞了,最終形成了死鎖。

大型知名項目踩坑記

上面列舉了4種 Mutex 使用中易錯的場景,也許你覺得這種簡單錯誤怎麼會犯呢?然而不少知名的項目,都或多或少犯過上面的錯誤。下面讓我們逐一欣賞一下。

gRPC

gRPC 是 Google 發起的一個 Go 實現的 RPC 框架。但即使是 Google 官方的框架,也出現了前面說的 Mutex 的使用問題。比如下面這個 issue (https://github.com/grpc/grpc-go/pull/795):

沒想到吧,竟然出現了把 Unlock 寫成 Lock 的錯誤🤭

docker

docker 的 issue-34881 本來是修復一個簡單的問題,如果不滿足條件判斷,就直接返回。你能看出有什麼問題嗎?

是的,你沒看錯,34行前面沒有解鎖 Unlock 就直接返回了。

當然,docker 還有許多 issue 都和 Mutex 有關。除此之外,其它的一些知名項目都踩過這種坑,所以,不能因為這幾種錯誤比較簡單就忽視它,還是要引起重視。

最後

最開始說了,這門課是晁嶽攀老師的《Go 並發編程實戰課》。3個月前就在極客時間上買了這個課程,一直拖到現在才開始看。剛看了前三講,很不錯,所以順帶推薦一下~~


都看到這裡了,不如順手點個 贊/在看?

相關焦點

  • linux下一種利用PTHREAD_MUTEX_ROBUST解決mutex死鎖的方法
    ] Attempting to lock the robust mutex.mutex consistent\n");s = pthread_mutex_consistent(&mtx);//調用函數進行更換鎖的屬主,也就是鎖從以前擁有者更換為當起線程if (s !
  • Go 性能分析工具 pprof 入門
    在實際項目中對高並發場景下的服務上線前必須經過pprof驗證,本文介紹了pprof的用法。以上幾種 Profile 可在 http://localhost:6060/debug/pprof/ 中看到,除此之外,go pprof 的 CMD 還包括:cmdline: 獲取程序的命令行啟動參數profile: 獲取指定時間內(從請求時開始)的cpuprof,倒計時結束後自動返回。參數: seconds, 默認值為30。
  • pthread_mutex_lock 引發的血案
    pthread_mutex_lock(&mymutex); pthread_mutex_unlock(&mymutex);對已鎖定的互斥對象上調用 pthread_mutex_lock() 的所有線程都將進入睡眠狀態,這些睡眠的線程將「排隊」訪問這個互斥對象。從上述可知,mutex會幫助我們鎖定一段邏輯區域的訪問。
  • 「GCTT 出品」在 go 中如何調用私有函數(綁定隱藏的標識符)
    2016 年 4 月 28 日名字在 golang 中的重要性和在其他任何一種語言是一樣的。他們甚至含有語義的作用:在一個包的外部某個名字的可見性是由這個名字首字母是否是大寫來決定的。有時為了更好的組織代碼或者在其他包使用某些隱藏的函數時需要克服這種限制。
  • 深度解密Go語言之pprof
    會遇到高並發、大流量,不靠譜的上下遊,突發的尖峰流量等等場景,這些都是不可預知的。線上突然大量報警,接口超時,錯誤數增加,除了看日誌、監控,就是用性能分析工具分析程序的性能,找到瓶頸。當然,一般這種情形不會讓你有機會去分析,降級、限流、回滾才是首先要做的,要先止損嘛。回歸正常之後,通過線上流量回放,或者壓測等手段,製造性能問題,再通過工具來分析系統的瓶頸。
  • 易錯周周練——名詞性從句之賓語從句
    A.which; whatB.which; whyC.that; howD.that; that4.Today we focus on _________ is called recurrent obesity (復發性肥胖) or yo-yo obesity (溜溜球式肥胖), _________ is the phenomenon
  • 初中英語重點時態之易錯點!知曉考點,看完你就能全對了!
    英語時態易錯點英語中動詞的時態很多種,很多同學碰到時態就懵了,不知道該用什麼時態。經常考試出錯,那這裡小姜說英語來講講時態中的一些易錯點!一般將來時一般將來時通俗點來講就是指將來發生的動作或者狀態,它的句子裡會出現表示將來的時間詞,比如tomorrow , in+一段時間(表示一段時間過後)next year 等等,有好幾種結構可以表示一般將來時
  • 最易錯的劍橋雅思聽力真題盤點
    這裡,新東方網雅思頻道為大家總結了最易錯的雅思劍橋聽力真題,供大家借鑑。   Test 1 Section 1 Q1.   入選理由:錄音原文:『We』ll go to the painting you like first…』本題中很多同學聽到painting,反應不過來就是Art Gallery的指代詞,直接寫了painting,但本題明顯要的是place,所以造成聽到了卻寫錯了。   Test 3 Section1 Q1.
  • 2021高考語文:「易錯成語」大盤點,一步一步來提分
    點讚關注,私信「高考」,有驚喜哦!以下就是有關於高考語文的成語易錯點了,想要提高成績的同學,一定要仔細的去看仔細的去學,不要再在考試的時候弄錯了。
  • 盤點4類英語對話場景 口語考試有秘訣
    盤點4類英語對話場景口語考試有秘訣  我們都知道在不同的場景需要不同的口語表達方式,如果在職場工作環境中說休閒娛樂的句子,那就不妙了。英語口語場景雖然有千千萬萬種,但都離不開四大主題,分別是工作、面試、人物、旅遊等方面,在這裡,我們為大家整理了其中幾個主題的學習小貼士,作為建議給同學們看看。
  • 高中化學最難的12種易錯考點和近300多道的易錯題集
    她後來說她的化學每次老師講好的題型,每次做下來的試卷,都會復盤一次,特別是高中化學典型的12種易錯知識點和300多道易錯題集!易錯的反覆摸透,就能一次次的提升上來!高中化學最難的無非就12種考點的易錯考點和易錯題集上!易錯知識點的復盤,就能徹底弄明白自己的知識點薄弱項在哪,然後針對性的進行吃透掌握!深入去剖析好易錯題集,看看自己易錯題在哪丟的分!
  • Go 並發任務編排利器之 WaitGroup
    協程組的個數有 n 個,執行 Add(n)協程組中,每個協程最後,執行方法 Done在協程組後面,執行 Wait 方法拿上面那個計算100萬個數之和再開根號的場景做示例:package mainimport ( "fmt" "math" "sync")func compute
  • 盤點最常用14種,不同場景換著用
    其實表達再見的方式很多,國家不同,地區不同,場景不同,甚至習慣不同,各有各的用法,今天西語菌就給大家盤點了最常用的14種西班牙語再見表達方式。4. Hasta la vista: 如果很長一段時間都不會再見面了,可以用這個短語進行告別。5. Hasta la próxima: 下回見的意思。6. Hasta maana: 明天見。7. Hasta el viernes: 周五見。
  • 高一到高三數學典型的37種易錯題型及1000多道易錯題大集
    不過,她說,她的最大的提分方法,高中數學其實很簡單,做好易錯點分析和易錯題集的反覆揣摩!自然而然,就能不斷的掌握吃透好每個數學的考點!特別是高中數學這非常典型37種易錯題型近1000多道易錯題集!高中數學不斷的進行易錯點的推敲,不斷的進行將自己的遺漏點推翻,的確是一個非常簡單,又非常見效的提分方法!但很多學生不知道怎麼做好自己的易錯題集!
  • Go 每日一庫之 go-bindata — 靜態資源嵌入詳解
    實際上提供的 CLI 工具在 go-bindata 子目錄裡,也就是 github.com/go-bindata/go-bindata/go-bindata/ ,三個 go-bindata 從前到後分別是 organ 名、項目名、目錄名。你可能會發現,這裡提供的地址,跟其他一些文章給的不一樣。
  • 新概念英語一,常考易錯知識點總結B,精選與解析
    英語中的冠詞有三種,一種是定冠詞,另一種是不定冠詞,還有一種是零冠詞。零冠詞(Zero articles )即是指名詞前面沒有不定冠詞( a、an )、定冠詞( the ),也沒有其他限定詞的現象。)---Does your father go to work by _____ car every day?