Go 系列教程 —— 25. Mutex

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。歡迎來到 [Golang 系列教程](https://studygolang.com/subject/2)的第 25 篇。 本教程我們學習 Mutex。我們還會學習怎樣通過 Mutex 和[通道](https://studygolang.com/articles/12402)來處理競態條件(Race Condition)。 ## 臨界區在學習 Mutex 之前,我們需要理解並發編程中臨界區(Critical Section)的概念。當程式並發地運行時,多個 [Go 協程](https://studygolang.com/articles/12342)不應該同時訪問那些修改共用資源的代碼。這些修改共用資源的代碼稱為臨界區。例如,假設我們有一段代碼,將一個變數 `x` 自增 1。 ```gox = x + 1```如果只有一個 Go 協程訪問上面的程式碼片段,那都沒有任何問題。 但當有多個協程並行執行階段,代碼卻會出錯,讓我們看看究竟是為什麼吧。簡單起見,假設在一行代碼的前面,我們已經運行了兩個 Go 協程。 在上一行代碼的內部,系統執行程式時分為如下幾個步驟(這裡其實還有很多包括寄存器的技術細節,以及加法的工作原理等,但對於我們的系列教程,只需認為只有三個步驟就好了): 1. 獲得 x 的當前值2. 計算 x + 13. 將步驟 2 計算得到的值賦值給 x如果只有一個協程執行上面的三個步驟,不會有問題。 我們討論一下當有兩個並發的協程執行該代碼時,會發生什麼。描述了當兩個協程並發地存取碼行 `x = x + 1` 時,可能出現的一種情況。 ![one-scenario](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/cs5.png) 我們假設 `x` 的初始值為 0。而協程 1 擷取 `x` 的初始值,並計算 `x + 1`。而在協程 1 將計算值賦值給 `x` 之前,系統環境切換到了協程 2。於是,協程 2 擷取了 `x` 的初始值(依然為 0),並計算 `x + 1`。接著系統上下文又切換回了協程 1。現在,協程 1 將計算值 1 賦值給 `x`,因此 `x` 等於 1。然後,協程 2 繼續開始執行,把計算值(依然是 1)複製給了 `x`,因此在所有協程執行完畢之後,`x` 都等於 1。 現在我們考慮另外一種可能發生的情況。 ![another-scenario](https://raw.githubusercontent.com/studygolang/gctt-images/master/golang-series/cs-6.png) 在上面的情形裡,協程 1 開始執行,完成了三個步驟後結束,因此 `x` 的值等於 1。接著,開始執行協程 2。目前 `x` 的值等於 1。而當協程 2 執行完畢時,`x` 的值等於 2。 所以,從這兩個例子你可以發現,根據環境切換的不同情形,`x` 的最終值是 1 或者 2。這種不太理想的情況稱為競態條件(Race Condition),其程式的輸出是由協程的執行順序決定的。 **在上例中,如果在任意時刻只允許一個 Go 協程訪問臨界區,那麼就可以避免競態條件。而使用 Mutex 可以達到這個目的**。 ## MutexMutex 用於提供一種加鎖機制(Locking Mechanism),可確保在某時刻只有一個協程在臨界區運行,以防止出現競態條件。 Mutex 可以在 [sync](https://golang.org/pkg/sync/) 包內找到。[Mutex](https://tip.golang.org/pkg/sync/#Mutex) 定義了兩個方法:[Lock](https://tip.golang.org/pkg/sync/#Mutex.Lock) 和 [Unlock](https://tip.golang.org/pkg/sync/#Mutex.Unlock)。所有在 `Lock` 和 `Unlock` 之間的代碼,都只能由一個 Go 協程執行,於是就可以避免競態條件。 ```gomutex.Lock() x = x + 1 mutex.Unlock() ```在上面的代碼中,`x = x + 1` 只能由一個 Go 協程執行,因此避免了競態條件。 如果有一個 Go 協程已經持有了鎖(Lock),當其他協程試圖獲得該鎖時,這些協程會被阻塞,直到 Mutex 解除鎖定為止。 ## 含有競態條件的程式在本節裡,我們會編寫一個含有競態條件的程式,而在接下來一節,我們再修複競態條件的問題。 ```gopackage main import ( "fmt" "sync" )var x = 0 func increment(wg *sync.WaitGroup) { x = x + 1 wg.Done()}func main() { var w sync.WaitGroup for i := 0; i < 1000; i++ { w.Add(1) go increment(&w) } w.Wait() fmt.Println("final value of x", x)}```在上述程式裡,第 7 行的 `increment` 函數把 `x` 的值加 1,並調用 [WaitGroup](https://studygolang.com/articles/12512) 的 `Done()`,通知該函數已結束。 在上述程式的第 15 行,我們產生了 1000 個 `increment` 協程。每個 Go 協程並發地運行,由於第 8 行試圖增加 `x` 的值,因此多個並發的協程試圖訪問 `x` 的值,這時就會發生競態條件。 由於 [playground](http://play.golang.org) 具有確定性,競態條件不會在 playground 發生,請在你的本地運行該程式。請在你的本地機器上多運行幾次,可以發現由於競態條件,每一次輸出都不同。我其中遇到的幾次輸出有 `final value of x 941`、`final value of x 928`、`final value of x 922` 等。 ## 使用 Mutex在前面的程式裡,我們建立了 1000 個 Go 協程。如果每個協程對 `x` 加 1,最終 `x` 期望的值應該是 1000。在本節,我們會在程式裡使用 Mutex,修複競態條件的問題。 ```gopackage main import ( "fmt" "sync" )var x = 0 func increment(wg *sync.WaitGroup, m *sync.Mutex) { m.Lock() x = x + 1 m.Unlock() wg.Done() }func main() { var w sync.WaitGroup var m sync.Mutex for i := 0; i < 1000; i++ { w.Add(1) go increment(&w, &m) } w.Wait() fmt.Println("final value of x", x)}```[在 playground 中運行](https://play.golang.org/p/VX9dwGhR62) [Mutex](https://golang.org/pkg/sync/#Mutex) 是一個結構體類型,我們在第 15 行建立了 `Mutex` 類型的變數 `m`,其值為零值。在上述程式裡,我們修改了 `increment` 函數,將增加 `x` 的代碼(`x = x + 1`)放置在 `m.Lock()` 和 `m.Unlock()`之間。現在這段代碼不存在競態條件了,因為任何時刻都只允許一個協程執行這段代碼。 於是如果運行該程式,會輸出: ```final value of x 1000```在第 18 行,傳遞 Mutex 的地址很重要。如果傳遞的是 Mutex 的值,而非地址,那麼每個協程都會得到 Mutex 的一份拷貝,競態條件還是會發生。 ## 使用通道處理競態條件我們還能用通道來處理競態條件。看看是怎麼做的。 ```gopackage main import ( "fmt" "sync" )var x = 0 func increment(wg *sync.WaitGroup, ch chan bool) { ch <- true x = x + 1 <- ch wg.Done() }func main() { var w sync.WaitGroup ch := make(chan bool, 1) for i := 0; i < 1000; i++ { w.Add(1) go increment(&w, ch) } w.Wait() fmt.Println("final value of x", x)}```[在 playground 中 運行](https://play.golang.org/p/M1fPEK9lYz) 在上述程式中,我們建立了容量為 1 的[緩衝通道](https://studygolang.com/articles/12512),並在第 18 行將它傳入 `increment` 協程。該緩衝通道用於保證只有一個協程訪問增加 `x` 的臨界區。具體的實現方法是在 `x` 增加之前(第 8 行),傳入 `true` 給緩衝通道。由於緩衝通道的容量為 1,所以任何其他協程試圖寫入該通道時,都會發生阻塞,直到 `x` 增加後,通道的值才會被讀取(第 10 行)。實際上這就保證了只允許一個協程訪問臨界區。 該程式也輸出: ```final value of x 1000 ```## Mutex vs 通道通過使用 Mutex 和通道,我們已經解決了競態條件的問題。那麼我們該選擇使用哪一個?答案取決於你想要解決的問題。如果你想要解決的問題更適用於 Mutex,那麼就用 Mutex。如果需要使用 Mutex,無須猶豫。而如果該問題更適用於通道,那就使用通道。:) 由於通道是 Go 語言很酷的特性,大多數 Go 新手處理每個並發問題時,使用的都是通道。這是不對的。Go 給了你選擇 Mutex 和通道的餘地,選擇其中之一都可以是正確的。 總體說來,當 Go 協程需要與其他協程通訊時,可以使用通道。而當只允許一個協程訪問臨界區時,可以使用 Mutex。 就我們上面解決的問題而言,我更傾向於使用 Mutex,因為該問題並不需要協程間的通訊。所以 Mutex 是很自然的選擇。 我的建議是去選擇針對問題的工具,而別讓問題去將就工具。:) 本教程到此結束。祝你愉快。 **上一教程 - [Select](https://studygolang.com/articles/12522)****下一教程 - [結構體取代類](https://studygolang.com/articles/12630)**

via: https://golangbot.com/mutex/

作者:Nick Coghlan 譯者:Noluye 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

2383 次點擊  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.