TOP

golang:1.并發編程之互斥鎖、讀寫鎖詳解(一)
2017-09-30 13:37:28 】 瀏覽:10050
Tags:

本文轉載自junjie,而后稍作修改。

一、互斥鎖

      互斥鎖是傳統的并發程序對共享資源進行訪問控制的主要手段。它由標準庫代碼包sync中的Mutex結構體類型代表。sync.Mutex類型(確切地說优乐棋牌app下载,是*sync.Mutex類型)只有兩個公開方法——Lock和Unlock。顧名思義,前者被用于鎖定當前的互斥量,而后者則被用來對當前的互斥量進行解鎖。

類型sync.Mutex的零值表示了未被鎖定的互斥量。也就是說,它是一個開箱即用的工具。我們只需對它進行簡單聲明就可以正常使用了,就像這樣:

代碼如下:
var mutex sync.Mutex

mutex.Lock()

在我們使用其他編程語言(比如C或Java)的鎖類工具的時候,可能會犯的一個低級錯誤就是忘記及時解開已被鎖住的鎖,從而導致諸如流程執行異常、線程執行停滯甚至程序死鎖等等一系列問題的發生。然而,在Go語言中,這個低級錯誤的發生幾率極低。其主要原因是有defer語句的存在。

      我們一般會在鎖定互斥鎖之后緊接著就用defer語句來保證該互斥鎖的及時解鎖。請看下面這個函數:

代碼如下:
var mutex sync.Mutex

func write() {

mutex.Lock()

defer mutex.Unlock()

// 省略若干條語句

}

函數write中的這條defer語句保證了在該函數被執行結束之前互斥鎖mutex一定會被解鎖。這省去了我們在所有return語句之前以及異常發生之時重復的附加解鎖操作的工作。在函數的內部執行流程相對復雜的情況下,這個工作量是不容忽視的,并且極易出現遺漏和導致錯誤。所以,這里的defer語句總是必要的。在Go語言中,這是很重要的一個慣用法。我們應該養成這種良好的習慣。

      對于同一個互斥鎖的鎖定操作和解鎖操作總是應該成對的出現。如果我們鎖定了一個已被鎖定的互斥鎖,那么進行重復鎖定操作的Goroutine將會被阻塞,直到該互斥鎖回到解鎖狀態。請看下面的示例:

代碼如下:

func repeatedlyLock() {

var mutex sync.Mutex

fmt.Println("Lock the lock. (G0)")

mutex.Lock()

fmt.Println("The lock is locked. (G0)")

for i := 1; i <= 3; i++ {

go func(i int) {

fmt.Printf("Lock the lock. (G%d)\n", i)

mutex.Lock()

fmt.Printf("The lock is locked. (G%d)\n", i)

}(i)

}

time.Sleep(time.Second)

fmt.Println("Unlock the lock. (G0)")

mutex.Unlock()

fmt.Println("The lock is unlocked. (G0)")

time.Sleep(time.Second)

}

我們把執行repeatedlyLock函數的Goroutine稱為G0。而在repeatedlyLock函數中,我們又啟用了3個Goroutine,并分別把它們命名為G1、G2和G3。可以看到优乐棋牌app下载,我們在啟用這3個Goroutine之前就已經對互斥鎖mutex進行了鎖定,并且在這3個Goroutine將要執行的go函數的開始處也加入了對mutex的鎖定操作。這樣做的意義是模擬并發地對同一個互斥鎖進行鎖定的情形。當for語句被執行完畢之后,我們先讓G0小睡1秒鐘,以使運行時系統有充足的時間開始運行G1、G2和G3。在這之后,解鎖mutex。為了能夠讓讀者更加清晰地了解到repeatedlyLock函數被執行的情況,我們在這些鎖定和解鎖操作的前后加入了若干條打印語句,并在打印內容中添加了我們為這幾個Goroutine起的名字。也由于這個原因,我們在repeatedlyLock函數的最后再次編寫了一條“睡眠”語句,以此為可能出現的其他打印內容再等待一小會兒。

     經過短暫的執行,標準輸出上會出現如下內容:

代碼如下:
Lock the lock. (G0)

The lock is locked. (G0)

Lock the lock. (G1)

Lock the lock. (G2)

Lock the lock. (G3)

Unlock the lock. (G0)

The lock is unlocked. (G0)

The lock is locked. (G1)

從這八行打印內容中,我們可以清楚的看出上述四個Goroutine的執行情況。首先,在repeatedlyLock函數被執行伊始,對互斥鎖的第一次鎖定操作便被進行并順利地完成。這由第一行和第二行打印內容可以看出。而后,在repeatedlyLock函數中被啟用的那三個Goroutine在G0的第一次“睡眠”期間開始被運行。當相應的go函數中的對互斥鎖的鎖定操作被進行的時候,它們都被阻塞住了。原因是該互斥鎖已處于鎖定狀態了。這就是我們在這里只看到了三個連續的Lock the lock. (G<i>)而沒有立即看到The lock is locked. (G<i>)的原因。隨后,G0“睡醒”并解鎖互斥鎖。這使得正在被阻塞的G1、G2和G3都會有機會重新鎖定該互斥鎖。但是,只有一個Goroutine會成功。成功完成鎖定操作的某一個Goroutine會繼續執行在該操作之后的語句。而其他Goroutine將繼續被阻塞优乐棋牌app下载,直到有新的機會到來。這也就是上述打印內容中的最后三行所表達的含義。顯然,G1搶到了這次機會并成功鎖定了那個互斥鎖。

      實際上,我們之所以能夠通過使用互斥鎖對共享資源的唯一性訪問進行控制正是因為它的這一特性。這有效的對競態條件進行了消除。

      互斥鎖的鎖定操作的逆操作并不會引起任何Goroutine的阻塞。但是,它的進行有可能引發運行時恐慌。更確切的講,當我們對一個已處于解鎖狀態的互斥鎖進行解鎖操作的時候,就會已發一個運行時恐慌。這種情況很可能會出現在相對復雜的流程之中——我們可能會在某個或多個分支中重復的加入針對同一個互斥鎖的解鎖操作。避免這種情況發生的最簡單、有效的方式依然是使用defer語句。這樣更容易保證解鎖操作的唯一性。

      雖然互斥鎖可以被直接的在多個Goroutine之間共享,但是我們還是強烈建議把對同一個互斥鎖的成對的鎖定和解鎖操作放在同一個層次的代碼塊中。例如,在同一個函數或方法中對某個互斥鎖的進行鎖定和解鎖。又例如,把互斥鎖作為某一個結構體類型中的字段,以便在該類型的多個方法中使用它。此外,我們還應該使代表互斥鎖的變量的訪問權限盡量的低。這樣才能盡量避免它在不相關的流程中被誤用,從而導致程序不正確的行為。

      互斥鎖是我們見到過的眾多同步工具

請關注公眾號獲取更多資料



首頁 上一頁 1 2 3 4 5 下一頁 尾頁 1/5/5
】【打印繁體】【】【】 【】【】【】 【關閉】 【返回頂部
上一篇5步搭建GO環境 下一篇轉發器---轉發網絡通信