Go sync.Mutex

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 釋放讀鎖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弥咪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子十绑,更是在濱河造成了極大的恐慌聚至,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件本橙,死亡現(xiàn)場離奇詭異扳躬,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)甚亭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門贷币,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人亏狰,你說我怎么就攤上這事役纹。” “怎么了暇唾?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵促脉,是天一觀的道長。 經(jīng)常有香客問我信不,道長嘲叔,這世上最難降的妖魔是什么亡呵? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任抽活,我火速辦了婚禮,結(jié)果婚禮上锰什,老公的妹妹穿的比我還像新娘下硕。我一直安慰自己,他們只是感情好汁胆,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布梭姓。 她就那樣靜靜地躺著,像睡著了一般嫩码。 火紅的嫁衣襯著肌膚如雪誉尖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天铸题,我揣著相機(jī)與錄音铡恕,去河邊找鬼。 笑死丢间,一個(gè)胖子當(dāng)著我的面吹牛探熔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播烘挫,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼诀艰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起其垄,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤苛蒲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后捉捅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撤防,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年棒口,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寄月。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡无牵,死狀恐怖漾肮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情茎毁,我是刑警寧澤克懊,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站七蜘,受9級(jí)特大地震影響谭溉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜橡卤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一扮念、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碧库,春花似錦柜与、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至沽瞭,卻和暖如春迁匠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驹溃。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國打工城丧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吠架。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓芙贫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親傍药。 傳聞我的和親對(duì)象是個(gè)殘疾皇子磺平,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容