鎖的介紹與使用
1 互斥鎖
傳統(tǒng)并發(fā)程序?qū)蚕碣Y源進(jìn)行訪問控制的主要手段,由標(biāo)準(zhǔn)庫代碼包中sync中的Mutex結(jié)構(gòu)體表示帮非。
//Mutex 是互斥鎖譬猫, 零值是解鎖的互斥鎖碌冶, 首次使用后不得復(fù)制互斥鎖。
type Mutex struct {
state int32
sema uint32
}
sync.Mutex類型只有兩個(gè)公開的指針方法
//Locker表示可以鎖定和解鎖的對(duì)象淘邻。
type Locker interface {
Lock()
Unlock()
}
//鎖定當(dāng)前的互斥量
//如果鎖已被使用窑眯,則調(diào)用goroutine
//阻塞直到互斥鎖可用屏积。
func (m *Mutex) Lock()
//對(duì)當(dāng)前互斥量進(jìn)行解鎖
//如果在進(jìn)入解鎖時(shí)未鎖定m,則為運(yùn)行時(shí)錯(cuò)誤磅甩。
//鎖定的互斥鎖與特定的goroutine無關(guān)炊林。
//允許一個(gè)goroutine鎖定Mutex然后安排另一個(gè)goroutine來解鎖它。
func (m *Mutex) Unlock()
聲明一個(gè)互斥鎖:
var mutex sync.Mutex
不像C或Java的鎖類工具更胖,我們可能會(huì)犯一個(gè)錯(cuò)誤:忘記及時(shí)解開已被鎖住的鎖铛铁,從而導(dǎo)致流程異常。但Go由于存在defer却妨,所以此類問題出現(xiàn)的概率極低饵逐。關(guān)于defer解鎖的方式如下:
var mutex sync.Mutex
func Write() {
mutex.Lock()
defer mutex.Unlock()
}
如果對(duì)一個(gè)已經(jīng)上鎖的對(duì)象再次上鎖,那么就會(huì)導(dǎo)致該鎖定操作被阻塞彪标,直到該互斥鎖回到被解鎖狀態(tài)
func main() {
var mutex sync.Mutex
fmt.Println("start lock main")
mutex.Lock()
fmt.Println("get locked main")
for i := 1;i<=3 ;i++ {
go func(i int) {
fmt.Println("start lock ",i)
mutex.Lock()
fmt.Println("get locked ",i)
}(i)
}
time.Sleep(time.Second)
fmt.Println("Unlock the lock main")
mutex.Unlock()
fmt.Println("get unlocked main")
time.Sleep(time.Second)
}
上面的示例中倍权,我們?cè)趂or循環(huán)之前開始加鎖,然后在每一次循環(huán)中創(chuàng)建一個(gè)協(xié)程捞烟,并對(duì)其加鎖薄声,但是由于之前已經(jīng)加鎖了,所以這個(gè)for循環(huán)中的加鎖會(huì)陷入阻塞直到main中的鎖被解鎖题画, time.Sleep(time.Second) 是為了能讓系統(tǒng)有足夠的時(shí)間運(yùn)行for循環(huán)默辨,輸出結(jié)果如下:
start lock main
get locked main
start lock 3
start lock 1
start lock 2
Unlock the lock main
get unlocked main
get locked 3
最終在main解鎖后,三個(gè)協(xié)程會(huì)重新?lián)寠Z互斥鎖權(quán)苍息,最終協(xié)程3獲勝缩幸。
互斥鎖鎖定操作的逆操作并不會(huì)導(dǎo)致協(xié)程阻塞,但是有可能導(dǎo)致引發(fā)一個(gè)無法恢復(fù)的運(yùn)行時(shí)的panic竞思,比如對(duì)一個(gè)未鎖定的互斥鎖進(jìn)行解鎖時(shí)就會(huì)發(fā)生panic表谊。避免這種情況的最有效方式就是使用defer。
我們知道如果遇到panic盖喷,可以使用recover方法進(jìn)行恢復(fù)爆办,但是如果對(duì)重復(fù)解鎖互斥鎖引發(fā)的panic卻是徒勞的(Go 1.8及以后)。
func main() {
defer func() {
fmt.Println("Try to recover the panic")
if p := recover();p!=nil{
fmt.Println("recover the panic : ",p)
}
}()
var mutex sync.Mutex
fmt.Println("start lock")
mutex.Lock()
fmt.Println("get locked")
fmt.Println("unlock lock")
mutex.Unlock()
fmt.Println("lock is unlocked")
fmt.Println("unlock lock again")
mutex.Unlock()
}
以上代碼試圖對(duì)重復(fù)解鎖引發(fā)的panic進(jìn)行recover课梳,但是我們發(fā)現(xiàn)操作失敗距辆,輸出結(jié)果:
start lock
get locked
fatal error: sync: unlock of unlocked mutex
unlock lock
lock is unlocked
unlock lock again
goroutine 1 [running]:
runtime.throw(0x4c2b46, 0x1e)
C:/Go/src/runtime/panic.go:619 +0x88 fp=0xc04207dea8 sp=0xc04207de88 pc=0x428978
sync.throw(0x4c2b46, 0x1e)
C:/Go/src/runtime/panic.go:608 +0x3c fp=0xc04207dec8 sp=0xc04207dea8 pc=0x4288dc
sync.(*Mutex).Unlock(0xc042060080)
C:/Go/src/sync/mutex.go:184 +0xc9 fp=0xc04207def0 sp=0xc04207dec8 pc=0x456b59
main.main()
D:/GoDemo/src/MyGo/Demo_04.go:23 +0x1dd fp=0xc04207df88 sp=0xc04207def0 pc=0x48ca9d
runtime.main()
C:/Go/src/runtime/proc.go:198 +0x20e fp=0xc04207dfe0 sp=0xc04207df88 pc=0x42a21e
runtime.goexit()
C:/Go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc04207dfe8 sp=0xc04207dfe0 pc=0x44f791
雖然互斥鎖可以被多個(gè)協(xié)程共享余佃,但還是建議將對(duì)同一個(gè)互斥鎖的加鎖解鎖操作放在同一個(gè)層次的代碼中。
2 讀寫鎖
讀寫鎖是針對(duì)讀寫操作的互斥鎖跨算,可以分別針對(duì)讀操作與寫操作進(jìn)行鎖定和解鎖操作 咙冗。
讀寫鎖的訪問控制規(guī)則如下:
①多個(gè)寫操作之間是互斥的
②寫操作與讀操作之間也是互斥的
③多個(gè)讀操作之間不是互斥的
在這樣的控制規(guī)則下,讀寫鎖可以大大降低性能損耗漂彤。
由標(biāo)準(zhǔn)庫代碼包中sync中的RWMutex結(jié)構(gòu)體表示
// RWMutex是一個(gè)讀/寫互斥鎖,可以由任意數(shù)量的讀操作或單個(gè)寫操作持有灾搏。
// RWMutex的零值是未鎖定的互斥鎖挫望。
//首次使用后,不得復(fù)制RWMutex狂窑。
//如果goroutine持有RWMutex進(jìn)行讀取而另一個(gè)goroutine可能會(huì)調(diào)用Lock媳板,那么在釋放初始讀鎖之前,goroutine不應(yīng)該期望能夠獲取讀鎖定泉哈。
//特別是蛉幸,這種禁止遞歸讀鎖定。 這是為了確保鎖最終變得可用; 阻止的鎖定會(huì)阻止新讀操作獲取鎖定丛晦。
type RWMutex struct {
w Mutex //如果有待處理的寫操作就持有
writerSem uint32 // 寫操作等待讀操作完成的信號(hào)量
readerSem uint32 //讀操作等待寫操作完成的信號(hào)量
readerCount int32 // 待處理的讀操作數(shù)量
readerWait int32 // number of departing readers
}
sync中的RWMutex有以下幾種方法:
//對(duì)讀操作的鎖定
func (rw *RWMutex) RLock()
//對(duì)讀操作的解鎖
func (rw *RWMutex) RUnlock()
//對(duì)寫操作的鎖定
func (rw *RWMutex) Lock()
//對(duì)寫操作的解鎖
func (rw *RWMutex) Unlock()
//返回一個(gè)實(shí)現(xiàn)了sync.Locker接口類型的值奕纫,實(shí)際上是回調(diào)rw.RLock and rw.RUnlock.
func (rw *RWMutex) RLocker() Locker
Unlock會(huì)試圖喚醒所有因欲進(jìn)行讀鎖定而被阻塞的協(xié)程,而 RUnlock 只會(huì)在已無任何讀鎖定的情況下烫沙,試圖喚醒一個(gè)因欲進(jìn)行寫鎖定而被阻塞的協(xié)程匹层。若對(duì)一個(gè)未被寫鎖定的讀寫鎖進(jìn)行寫解鎖,就會(huì)引發(fā)一個(gè)不可恢復(fù)的panic锌蓄,同理對(duì)一個(gè)未被讀鎖定的讀寫鎖進(jìn)行讀寫鎖也會(huì)如此升筏。
由于讀寫鎖控制下的多個(gè)讀操作之間不是互斥的,因此對(duì)于讀解鎖更容易被忽視瘸爽。對(duì)于同一個(gè)讀寫鎖您访,添加多少個(gè)讀鎖定,就必要有等量的讀解鎖剪决,這樣才能其他協(xié)程有機(jī)會(huì)進(jìn)行操作灵汪。
func main() {
var rwm sync.RWMutex
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println("try to lock read ", i)
rwm.RLock()
fmt.Println("get locked ", i)
time.Sleep(time.Second *2)
fmt.Println("try to unlock for reading ", i)
rwm.RUnlock()
fmt.Println("unlocked for reading ", i)
}(i)
}
time.Sleep(time.Millisecond * 1000)
fmt.Println("try to lock for writing")
rwm.Lock()
fmt.Println("locked for writing")
}
上面的示例創(chuàng)建了三個(gè)協(xié)程用于對(duì)讀寫鎖的讀鎖定與讀解鎖操作。在 rwm.Lock()種會(huì)對(duì)main中協(xié)程進(jìn)行寫鎖定昼捍,但是for循環(huán)中的讀解鎖尚未完成识虚,因此會(huì)造成mian中的協(xié)程阻塞。當(dāng)for循環(huán)中的讀解鎖操作都完成后就會(huì)試圖喚醒main中阻塞的協(xié)程妒茬,main中的寫鎖定才會(huì)完成担锤。輸出結(jié)果如下
try to lock read 0
get locked 0
try to lock read 2
get locked 2
try to lock read 1
get locked 1
try to lock for writing
try to unlock for reading 0
unlocked for reading 0
try to unlock for reading 2
unlocked for reading 2
try to unlock for reading 1
unlocked for reading 1
locked for writing