Go語言提供了sync
包和channel
機(jī)制來解決并發(fā)機(jī)制中不同goroutine
之間的同步和通信
鎖 Locker
Go語言使用go
語句開啟新的goroutine
旭贬,由于goroutine
非常輕量除了對(duì)其分配棧空間外捧杉,所占的空間也是微乎其微的。但當(dāng)多個(gè)goroutine
同時(shí)處理時(shí)會(huì)遇到比如同時(shí)搶占一個(gè)資源蕉汪,某個(gè)goroutine
會(huì)等待等一個(gè)goroutine
處理完畢某才能繼續(xù)執(zhí)行的問題谬哀。對(duì)于這種情況,官方并不希望依靠共享內(nèi)存的方式來實(shí)現(xiàn)進(jìn)程的協(xié)同操作节沦,而是希望通過channel
信道的方式來處理。但在某些特殊情況下础爬,依然需要使用到鎖甫贯,為此sync
包提供了鎖。
當(dāng)在并發(fā)情況下看蚜,多個(gè)goroutine
同時(shí)修改某一個(gè)變量時(shí)叫搁,就會(huì)出現(xiàn)資源搶占,因此會(huì)導(dǎo)致數(shù)據(jù)不一致的問題供炎。
package main
import (
"fmt"
"time"
)
func main() {
var num = 0
for i := 0; i < 100000; i++ {
go func(i int) {
num++
fmt.Printf("goroutine %d: num = %d\n", i, num)
}(i)
}
time.Sleep(time.Second)//主goroutine等待1秒以確保所有工作goroutine執(zhí)行完畢
}
...
goroutine 10018: num = 98635
goroutine 12646: num = 98635
goroutine 12844: num = 98635
goroutine 12950: num = 98635
...
上例中goroutine
一依次從寄存器中讀取num
的值后做加法運(yùn)算渴逻,然后將其結(jié)果回寫到寄存器中。運(yùn)行中會(huì)發(fā)現(xiàn)存在一個(gè)goroutine
取出num
的值時(shí)做加法運(yùn)算時(shí)音诫,另一個(gè)goroutine
也取出了num
的值惨奕。因?yàn)樯弦粋€(gè)goroutine
運(yùn)行結(jié)果還沒有回寫到寄存器,最終導(dǎo)致多個(gè)goroutine
產(chǎn)生的相同的結(jié)果竭钝。
并發(fā)編程中同步原語-鎖梨撞,為了保證多個(gè)線程或goroutine
在訪問同一塊內(nèi)存時(shí)不出現(xiàn)混亂,Go語言的sync
包提供了常見的并發(fā)編程同步原語的控制鎖香罐。
sync
包圍繞著Locker
鎖接口展開卧波,Locker
接口中提供了兩個(gè)方法Lock()
和Unlock()
。
type Locker interface {
Lock()
Unlock()
}
Go語言標(biāo)準(zhǔn)庫sync
中提供了兩種鎖分別是互斥鎖sync.Mutex
和讀寫互斥鎖sync.RWMutex
互斥鎖sync.Mutex
sync.Mutex是一個(gè)互斥鎖庇茫,可以由不同的goroutine加鎖和解鎖港粱。
sync.Mutex
是Golang標(biāo)準(zhǔn)庫提供的一個(gè)互斥鎖,當(dāng)一個(gè)goroutine
獲得互斥鎖權(quán)限后旦签,其他請求鎖的goroutine
會(huì)阻塞在Lock()
方法的調(diào)用上查坪,直到調(diào)用Unlock()
方法被釋放锈颗。
例如:10個(gè)并發(fā)的goroutine
打印同一個(gè)數(shù)字100,為避免重復(fù)打印咪惠,實(shí)現(xiàn)printOnce(num int)
函數(shù),使用集合set
記錄已打印過的數(shù)字淋淀。若數(shù)字已經(jīng)打印過遥昧,則不再打印。
$ vim mutex_test.go
package test
import (
"fmt"
"testing"
"time"
)
var set = make(map[int]bool, 0)
func printOnce(index int, num int) {
if _, ok := set[num]; !ok {
fmt.Println(index, num)
}
set[num] = true
}
func TestPrint(t *testing.T) {
for i := 0; i < 10; i++ {
go printOnce(i, 100)
}
time.Sleep(time.Second)
}
$ go test -v mutex_test.go
=== RUN TestPrint
9 100
3 100
--- PASS: TestPrint (1.00s)
PASS
ok command-line-arguments 1.304s
程序多次運(yùn)行后會(huì)發(fā)現(xiàn)打印次數(shù)多次朵纷,因?yàn)閷?duì)同一個(gè)數(shù)據(jù)結(jié)構(gòu)set
的訪問發(fā)生了沖突炭臭。
并發(fā)訪問中比如多個(gè)goroutine
并發(fā)更新同一個(gè)資源,比如計(jì)時(shí)器袍辞、賬戶余額鞋仍、秒殺系統(tǒng)、向同一個(gè)緩存中并發(fā)寫入數(shù)據(jù)等等搅吁。如果沒有互斥控制威创,很容易會(huì)出現(xiàn)異常,比如計(jì)時(shí)器計(jì)數(shù)不準(zhǔn)確谎懦、用戶賬戶可能出現(xiàn)透支肚豺、秒殺系統(tǒng)出現(xiàn)超賣、緩存出現(xiàn)數(shù)據(jù)緩存等等界拦,后果會(huì)很嚴(yán)重吸申。
互斥鎖是并發(fā)控制的一種基本手段,是為了避免競爭而建立的一種并發(fā)控制機(jī)制享甸。學(xué)習(xí)前首先需要弄清楚一個(gè)概念-臨界區(qū)截碴。在并發(fā)編程中,如果程序中的一部分會(huì)被并發(fā)訪問或修改蛉威,為了避免并發(fā)訪問導(dǎo)致的意想不到的結(jié)果日丹,這部分程序需要被保護(hù)起來,這部分被保護(hù)起來的程序就叫做臨界區(qū)蚯嫌。
臨界區(qū)是一個(gè)被共享的資源聚凹,或者說是一個(gè)整體的共享資源,比如對(duì)數(shù)據(jù)庫的訪問齐帚,對(duì)某個(gè)共享數(shù)據(jù)結(jié)構(gòu)的操作妒牙。對(duì)一個(gè)I/O設(shè)備的使用,對(duì)一個(gè)連接池中的連接的調(diào)用等等对妄。
如果很多線程同步訪問臨界區(qū)就會(huì)造成訪問或操作錯(cuò)誤湘今,這并不是我們希望看到的結(jié)果。所以剪菱,使用互斥鎖摩瞎,限定臨界區(qū)只能同時(shí)由一個(gè)線程持有拴签。當(dāng)臨界區(qū)由一個(gè)線程持有的時(shí)候,其他線程如果想進(jìn)入臨界區(qū)就會(huì)返回失敗旗们,或者是等待蚓哩。直到持有的線程退出臨界區(qū),這些等待線程中的某一個(gè)才有機(jī)會(huì)接著持有這個(gè)臨界區(qū)上渴。
互斥鎖可以很好的解決資源競爭的問題岸梨,因此也有人稱之為排它鎖。Golang標(biāo)準(zhǔn)庫中使用Mutex來實(shí)現(xiàn)互斥鎖稠氮。根據(jù)2019年分析Go并發(fā)Bug的論文Understanding Real-World Concurrency Bugs in Go中曹阔,Mutex是使用最為廣泛的同步原語(Synchronization primitives, 并發(fā)原語或同步原語)隔披。關(guān)于同步原語并沒有一個(gè)嚴(yán)格的定義赃份,可將其看作是解決并發(fā)問題的一個(gè)基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu)。
type Mutex struct {
state int32 //狀態(tài)標(biāo)識(shí)
sema uint32 //信號(hào)量
}
Go標(biāo)準(zhǔn)庫提供了sync.Mutex
互斥鎖類型以及兩個(gè)方法分別是Lock
加鎖和Unlock
釋放鎖奢米∽ズ可以通過在代碼前調(diào)用Lock
方法,在代碼后調(diào)用Unlock
方法來保證一段代碼的互斥執(zhí)行鬓长,也可以使用defer
語句來保證互斥鎖一定會(huì)被解鎖园蝠。當(dāng)一個(gè)goroutine
調(diào)用Lock
方法獲得鎖后,其它請求的goroutine
都會(huì)阻塞在Lock
方法直到鎖被釋放痢士。
一個(gè)互斥鎖只能同時(shí)被一個(gè)goroutine
鎖定彪薛,其它goroutine
將阻塞直到互斥鎖被解鎖,也就是重新爭搶對(duì)互斥鎖的鎖定怠蹂。需要注意的是善延,對(duì)一個(gè)未鎖定的互斥鎖解鎖時(shí)將會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤。
sync.Mutex
不區(qū)分讀寫鎖城侧,只有Lock()
和Lock()
之間才會(huì)導(dǎo)致阻塞的情況易遣。若在一個(gè)地方Lock()
,在另一個(gè)地方不Lock()
而是直接修改或訪問共享數(shù)據(jù)嫌佑,對(duì)于sync.Mutext
類型是允許的豆茫,因?yàn)?code>mutex不會(huì)和goroutine
進(jìn)行關(guān)聯(lián)。若要區(qū)分讀鎖和寫鎖屋摇,可使用sync.RWMutex
類型揩魂。
在Lock()
和Unlock()
之間的代碼段成為資源臨界區(qū)(critical section),在這一區(qū)間內(nèi)的代碼是嚴(yán)格被Lock()
保護(hù)的炮温,是線程安全的火脉,任何一個(gè)時(shí)間點(diǎn)都只能有一個(gè)goroutine
執(zhí)行這段區(qū)間的代碼。
例如:使用互斥鎖的Lock()
和Unlock()
方法將沖突包裹
package test
import (
"fmt"
"sync"
"testing"
"time"
)
var m sync.Mutex
var set = make(map[int]bool, 0)
func printOnce(index int, num int) {
m.Lock()
defer m.Unlock()
if _, ok := set[num]; !ok {
fmt.Println(index, num)
}
set[num] = true
}
func TestPrint(t *testing.T) {
for i := 0; i < 10; i++ {
go printOnce(i, 100)
}
time.Sleep(time.Second)
}
相同的數(shù)字只會(huì)比打印一次,當(dāng)一個(gè)goroutine
調(diào)用了Lock()
方法時(shí)倦挂,其他goroutine
被阻塞了畸颅,直到Unlock()
調(diào)用將鎖釋放。因此被包裹部分的代碼就能避免沖突方援,實(shí)現(xiàn)互斥没炒。
互斥即不能同時(shí)運(yùn)行,使用互斥鎖的兩個(gè)代碼片段相互排斥犯戏,只有其中一個(gè)代碼片段執(zhí)行完畢后送火,另一個(gè)才能執(zhí)行。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var num = 0
var locker sync.Mutex
for i := 0; i < 100000; i++ {
go func(i int) {
locker.Lock()
defer locker.Unlock()
num++
fmt.Printf("goroutine %d: num = %d\n", i, num)
}(i)
}
time.Sleep(time.Second)
}
從上例中可以發(fā)現(xiàn)笛丙,添加互斥鎖后,不僅沒有出現(xiàn)搶占資源導(dǎo)致的重復(fù)輸出假颇,而且輸出結(jié)果順序遞增胚鸯。
Go語言中的Mutex類型的互斥鎖Lock()
鎖定與其它語言不同的是,Lock()
鎖定的是互斥鎖而非一段代碼笨鸡。其他語言比如Java中使用同步鎖鎖定的是一段代碼姜钳,以確保多線程并發(fā)誓只有一個(gè)線程可以控制運(yùn)行此代碼塊直到釋放同步鎖。Go語言是在goroutine
中鎖定互斥鎖形耗,其它goroutine
執(zhí)行到有鎖的位置時(shí)哥桥,由于獲取不到互斥鎖的鎖定,因此會(huì)發(fā)生阻塞而等待激涤,從而達(dá)到控制同步的目的拟糕。
race detector
Golang提供了一個(gè)檢測并發(fā)訪問共享資源是否存在問題的工具 - race detector,它可以幫助自動(dòng)發(fā)現(xiàn)城中有沒有data race的問題倦踢。
Golang的race detector是基于Google的C/C++ sanitizers技術(shù)實(shí)現(xiàn)的送滞,編譯器通過探測所有的內(nèi)存訪問,加入代碼能監(jiān)視對(duì)這些內(nèi)存地址的訪問(讀或是寫)辱挥。代碼運(yùn)行時(shí)race detector能監(jiān)控對(duì)共享變量的非同步訪問犁嗅,出現(xiàn)race時(shí)就會(huì)打印出警告信息。
https://blog.golang.org/race-detector
讀寫鎖sync.RWMutex
在銀行存取錢時(shí)晤碘,對(duì)賬戶余額的修改是需要加鎖的褂微,因此此時(shí)可能有人匯款到你的賬戶,如果對(duì)金額的修改不加鎖园爷,很可能導(dǎo)致最后的金額發(fā)生錯(cuò)誤宠蚂。讀取賬戶余額也需要等待修改操作結(jié)束,才能讀取到正確的余額童社。大部分情況下肥矢,讀取余額的操作會(huì)更加頻繁,如果能保證讀取余額的操作能并發(fā)執(zhí)行,程序的效率會(huì)很大地提升甘改。
保證讀操作的安全旅东,只需要保證并發(fā)讀時(shí)沒有寫操作在進(jìn)行就行。在這種場景下就需要一種特殊類型的鎖十艾,即允許多個(gè)只讀操作并行執(zhí)行抵代,但寫操作會(huì)完全互斥。這種鎖稱之為多讀單寫鎖(multiple readers, single writer lock)忘嫉,簡稱讀寫鎖荤牍。讀寫鎖分為讀鎖和寫鎖,讀鎖允許同時(shí)執(zhí)行庆冕,但寫鎖是互斥的康吵。
sync.RWMutex
讀寫鎖是基于sync.Mutex
實(shí)現(xiàn)的,讀寫鎖的特點(diǎn)是針對(duì)讀寫操作的互斥鎖访递,讀寫鎖與互斥鎖最大不同之處在于分別對(duì)讀晦嵌、寫進(jìn)行了鎖定。一般用在大量讀操作少量寫操作中拷姿。
- 同時(shí)只能具有一個(gè)
goroutine
能夠獲得寫鎖定 - 同時(shí)可以具有任意多個(gè)
goroutine
獲得讀鎖定 - 同時(shí)只能存在寫鎖定或讀鎖定惭载,即讀和寫互斥。
換句話說
- 當(dāng)只有一個(gè)
goroutine
獲得寫鎖定時(shí)响巢,其它無論是讀鎖定還是寫鎖定都將會(huì)阻塞直到寫解鎖描滔。 - 當(dāng)只有一個(gè)
goroutine
獲得讀鎖定時(shí),其它讀鎖定仍然可以繼續(xù)執(zhí)行踪古。 - 當(dāng)有一個(gè)或多個(gè)讀鎖定時(shí)含长,寫鎖定將等待所有讀鎖定解鎖之后才能進(jìn)行寫鎖定。
這里所謂的讀鎖定(RLock)目的是為了告訴寫鎖定(Lock)伏穆,此時(shí)有很多人正在讀取數(shù)據(jù)茎芋,寫鎖定需要排隊(duì)等待。
一般來說蜈出,讀寫鎖會(huì)分為幾種情況:
- 讀鎖之間不互斥田弥,在沒有寫鎖的情況下,讀鎖是無堵塞的铡原,多個(gè)
goroutine
可以同時(shí)獲得讀鎖偷厦。 - 寫鎖之間是互斥的,當(dāng)存在寫鎖時(shí)燕刻,其它寫鎖會(huì)阻塞只泼。
- 寫鎖與讀鎖互斥,若存在讀鎖則寫鎖阻塞卵洗,若存在寫鎖則讀鎖阻塞请唱。
Go標(biāo)準(zhǔn)庫sync.RWMutex
讀寫互斥鎖提供了四個(gè)方法
讀寫互斥鎖 | 描述 |
---|---|
Lock | 添加寫鎖 |
Unlock | 釋放寫鎖 |
RLock | 添加讀鎖 |
RUnlock | 釋放讀鎖 |