第九章 基于共享變量的并發(fā)(三)鎖

使用鎖的一般流程

一、互斥鎖

思想

對(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í)候才適合使用讀寫鎖:

  1. 多數(shù)嘗試獲取該鎖的goroutines都是讀操作
  2. 該鎖在競(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


  1. 第九章 基于共享變量的并發(fā)(一)競(jìng)爭(zhēng)條件 ?

  2. Synchronization optimizations in Mustang ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容