
一、互斥鎖
思想
對(duì)資源A,同一時(shí)刻只能由一個(gè)goroutine占有
實(shí)現(xiàn)
1. 模式
監(jiān)控(monitor)模式:一個(gè)或多個(gè)變量被封裝起來(lái),只暴露(exported)一些函數(shù)/方法作為訪問(wèn)這些變量的唯一方式。每個(gè)函數(shù)在一開始就獲取互斥鎖并在最后釋放鎖,從而保證共享變量不會(huì)被并發(fā)訪問(wèn)。
與之前的文章[1]中提到的"monitor goroutine"用法類似,都是使用一個(gè)代理人(broker)來(lái)保證變量被順序訪問(wèn),互斥鎖使用的是一個(gè)二元信號(hào)量。
二元信號(hào)量(binary semaphore):一個(gè)只能為1或0的信號(hào)量
2. 使用channel
使用buffer大小為1的channel來(lái)保證最多只有一個(gè)goroutine在同一時(shí)刻訪問(wèn)一個(gè)共享變量
// create a binary semaphore (global variable)
var sema = make(chan struct{}, 1)
var resource int
// in each goroutine
// acquire token
sema <- struct{}{} //struct{}是類型,第二個(gè){}是初始化內(nèi)容
// use resource
resource = 1
// release token
<- sema
2. 使用sync.Mutex
Go的sync包里提供了sync.Mutex類型互斥鎖。使用Lock()方法嘗試獲取鎖,若已被其他goroutine獲取則會(huì)阻塞直到其他goroutine釋放;使用Unlock()方法釋放鎖
// create lock
var mu sync.Mutex
var resource int // mutex保護(hù)的變量一般在mutex聲明后立即聲明
// in each goroutine
// acquire lock
mu.Lock()
// use resource
resource = 1
// release lock
mu.Unlock()
臨界區(qū)(critical section):Lock()和Unlock()之間的內(nèi)容
無(wú)論哪條路徑都要釋放
其他的goroutine只有在當(dāng)前持有鎖的goroutine調(diào)用Unlock()之后才能獲得鎖,因此必須保證在goroutine結(jié)束之后持有的鎖被釋放。無(wú)論以哪條路徑通過(guò)函數(shù),包括錯(cuò)誤路徑。在結(jié)構(gòu)復(fù)雜的代碼中,很難靠人去判斷Lock()和Unlock()是否在所有路徑中都嚴(yán)格配對(duì),因此需要Go中的defer來(lái)調(diào)用Unlock()。
使用defer關(guān)鍵字
使用defer來(lái)調(diào)用Unlock()時(shí),臨界區(qū)會(huì)隱式地延伸到函數(shù)作用域的最后,即使在臨界區(qū)發(fā)生panic的時(shí)候也會(huì)執(zhí)行。由Go自動(dòng)完成。
func UseResource() int {
mu.Lock()
defer mu.Unlock() // 在return之后執(zhí)行
return resource
}
defer調(diào)用的成本比直接調(diào)用Unlock()會(huì)高一些,是為了代碼的可讀性和整潔度做出的trade-off。
As always with concurrent programs, favor clarity and resist premature optimization.
sync.Mutex不具可重入性(re-entrant)
// ***錯(cuò)誤示例***
func Op1() {
mu.Lock()
defer mu.Unlock()
Op2() // 錯(cuò)誤,會(huì)死鎖,會(huì)卡在Op2()中的mu.Lock()
// do something
}
func Op2() {
mu.Lock()
defer mu.Unlock()
// do something
}
當(dāng)goroutine A調(diào)用Op1()時(shí),在進(jìn)入Op2()后無(wú)法再次獲得互斥鎖,即使占用該鎖的goroutine是當(dāng)前goroutine A。
對(duì)應(yīng)的是Java中的java.util.concurrent.locks.ReentrantLock,這個(gè)可重入的,同一線程內(nèi)部調(diào)用的方法可以再次獲得當(dāng)前線程持有的鎖。
一個(gè)通用的解決方案:將一個(gè)函數(shù)分離為多個(gè)函數(shù),比如分離出的實(shí)際操作數(shù)據(jù)的函數(shù)subOp2(),該函數(shù)默認(rèn)goroutine在進(jìn)入該函數(shù)前就已經(jīng)持有相應(yīng)的互斥鎖。
// 正確示例
func Op1() {
mu.Lock()
defer mu.Unlock()
subOp2() // subOp2()并不去嘗試獲取鎖
// do something
}
func Op2() {
mu.Lock()
defer mu.Unlock()
subOp2()
}
func subOp2() {
// do something
}
二、讀寫鎖
sync.RWMutex:多讀單寫鎖(multiple readers, single writer lock)
var mu sync.RWMutex
// 讀鎖
// 只與寫鎖互斥,與其他占用讀鎖的goroutine不互斥
mu.RLock() // 獲取讀鎖
mu.RUnlock() // 釋放讀鎖
// 寫鎖
// 與讀鎖和寫鎖都互斥,跟sync.Mutex表現(xiàn)一致
mu.Lock() //獲取寫鎖
mu.Unlock() //釋放寫鎖
何時(shí)使用
因?yàn)镽WMutex的開銷較大,故滿足以下兩個(gè)條件的時(shí)候才適合使用讀寫鎖:
- 多數(shù)嘗試獲取該鎖的goroutines都是讀操作
- 該鎖在競(jìng)爭(zhēng)條件下(under contention)
鎖的競(jìng)爭(zhēng)條件
根據(jù)可能同時(shí)嘗試獲取該鎖的goroutine的數(shù)量來(lái)區(qū)分
競(jìng)爭(zhēng)鎖:goroutine經(jīng)常需要被阻塞等待來(lái)獲取該鎖
非競(jìng)爭(zhēng)鎖:一個(gè)goroutine來(lái)需求該鎖的時(shí)候沒(méi)有別的goroutine來(lái)競(jìng)爭(zhēng)(注意不一定自始至終只有一個(gè)goroutine,只是不會(huì)同時(shí)來(lái)嘗試獲?。?/p>
實(shí)際應(yīng)用中通常分為兩類:經(jīng)常性競(jìng)爭(zhēng)(mostly contended)和非經(jīng)常性競(jìng)爭(zhēng)(mostly uncontended)。一般通過(guò)提升非競(jìng)爭(zhēng)鎖的性能來(lái)優(yōu)化程序整體的性能[2]。
1/20/2018