在Go語(yǔ)言中限嫌,不僅有channel這類(lèi)比較易用且高級(jí)的同步機(jī)制,還有sync.Mutex 时捌、sync.WaitGroup等比較原始的同步機(jī)制怒医。通過(guò)它們,我們可以更加靈活的控制數(shù)據(jù)的同步和多協(xié)程的并發(fā)奢讨。
資源競(jìng)爭(zhēng)
在一個(gè)goroutine中稚叹,如果分配的內(nèi)存沒(méi)有被其他goroutine訪問(wèn),之后在該goroutine中是喲和哪個(gè)拿诸,那么不存在資源競(jìng)爭(zhēng)問(wèn)題扒袖。但如果同一塊內(nèi)容被多個(gè)goroutine同時(shí)訪問(wèn),就會(huì)產(chǎn)生不知道誰(shuí)先訪問(wèn)也就無(wú)法預(yù)料結(jié)果亩码。這就是資源競(jìng)爭(zhēng)季率,這塊內(nèi)存也可以稱(chēng)為**共享資源**。
// 共享資源
sum := 0
func main () {
// 開(kāi)啟100個(gè)協(xié)程 sum+10
for i:=0; i <100; i++ {
go add(10)
}
// 防止提前退出
tiem.Sleep(2 * time.Second)
fmt.Println("和為:"描沟, sum)
}
func add(i int) {
sum += i
}
// 示例中飒泻,結(jié)果可能是990或者980,并不是預(yù)期的1000吏廉。導(dǎo)致這種情況發(fā)送的核心原因是sum并不是并發(fā)安全的泞遗,因?yàn)橥瑫r(shí)有多個(gè)協(xié)程交叉執(zhí)行 sum += i,產(chǎn)生不可預(yù)料的結(jié)果。
**小技巧**:使用go build席覆、go run史辙、go test這些Go語(yǔ)言工具鏈提供的命令式,添加-race標(biāo)記可以幫我們檢查Go語(yǔ)言代碼是否存在資源競(jìng)爭(zhēng)佩伤。
同步原語(yǔ)
sync.Mutex
互斥鎖聊倔,顧名思義,指的是同一時(shí)刻只有一個(gè)協(xié)程執(zhí)行某段代碼生巡,其它協(xié)程都要等待該協(xié)程執(zhí)行完才能繼續(xù)執(zhí)行方库。 改造上述示例,聲明一個(gè)互斥鎖mutex障斋,修改add函數(shù)纵潦,訪問(wèn)共享資源的代碼片段就并發(fā)安全了徐鹤。
sum:= 0
mutex := sync.Mutex
func add(i int) {
mutex.Lock()
defer mutex.Unlock()
sum += i
}
// 被解鎖保護(hù)的sum += i 代碼片段又稱(chēng)為臨界區(qū)。在同步原語(yǔ)設(shè)計(jì)中邀层,臨界區(qū)指的是一個(gè)訪問(wèn)共享資源的代碼片段返敬,而這些共享資源又有無(wú)法同時(shí)被多個(gè)協(xié)程訪問(wèn)的特征。當(dāng)有協(xié)程進(jìn)入臨界區(qū)段時(shí)寥院,其它協(xié)程必須等待劲赠,這樣就保證了臨界區(qū)的并發(fā)安全。
互斥鎖的使用非常簡(jiǎn)單秸谢,它只有兩個(gè)方法Lock()和Unlock()凛澎,代表加鎖和解鎖。當(dāng)一個(gè)協(xié)程獲得Mutex鎖后估蹄,其它協(xié)程只能等到Mutex鎖釋放后才能再次獲得鎖塑煎。
Muetx 的 Lock 和 Unlock 方法總是成對(duì)出現(xiàn),而且要確保Lock 獲得鎖后臭蚁,一定執(zhí)行Unlock釋放鎖最铁,所以在函數(shù)或方法中會(huì) **采用defer 語(yǔ)句釋放鎖**。 這樣可以確保所以丁被釋放垮兑,不會(huì)被遺忘冷尉。
sync.RWMutex
sync.Muetx對(duì)共享資源進(jìn)行了加鎖,保證并發(fā)安全系枪。如果讀取操作也是多個(gè)協(xié)程呢雀哨?
func main() {
for i := 0; i < 100; i ++ {
go add(10)
}
for i := 1; i <10 ; i++ {
go fmt.Println("和為:", getSum())
}
time.Sleep(2 * time.Second)
}
func getSum() int {
b := sum
return b
}
// 開(kāi)啟了10個(gè)協(xié)程私爷,同時(shí)讀取sum的值震束。因?yàn)間etSum函數(shù)沒(méi)有任何加鎖控制,所以它不是并發(fā)安全的当犯,即一個(gè)goroutine在執(zhí)行sum+=i 操作時(shí),另一個(gè)goroutine可能在執(zhí)行 b:=sum 操作割疾,這就會(huì)導(dǎo)致讀取的sum值是一個(gè)過(guò)期的值嚎卫,結(jié)果不可預(yù)期, 要解決資源競(jìng)爭(zhēng)問(wèn)題宏榕,可以使用互斥鎖sync.Mutex
func getSum() int {
mutex.Lock()
defer mutex.Unlock()
b := sum
return b
}
因?yàn)閍dd 和 getSum函數(shù)使用的是同一個(gè)sync.Muetx拓诸,所以它們的操作是互斥的,也就是一個(gè)goroutine進(jìn)行修改操作時(shí)麻昼,另一個(gè)goroutine讀取操作會(huì)等待奠支,直到修改操作完畢。
我們解決了goroutine同時(shí)讀寫(xiě)的資源競(jìng)爭(zhēng)問(wèn)題抚芦,但是又遇到了另一個(gè)問(wèn)題——性能倍谜。因?yàn)槊看巫x寫(xiě)共享資源都要加鎖迈螟,所以性能底下,如何解決呢尔崔?
讀寫(xiě)這個(gè)特殊的場(chǎng)景答毫,有以下幾種情況:
1. 寫(xiě)的時(shí)候不能同時(shí)讀,因?yàn)檫@個(gè)時(shí)候讀取的話可能讀到臟數(shù)據(jù)
2. 讀的時(shí)候不能同時(shí)寫(xiě)季春,因?yàn)橐部赡墚a(chǎn)生不可預(yù)料的結(jié)果
3. 讀的時(shí)候可以同時(shí)讀洗搂,因?yàn)閿?shù)據(jù)不會(huì)改變,所以不管多少個(gè)goroutine讀都是并發(fā)安全的
所以可以通過(guò)sync.RWMutex 來(lái)優(yōu)化此段代碼载弄,提升性能耘拇。改用讀寫(xiě)鎖,來(lái)實(shí)現(xiàn)我們想要的結(jié)果
var mutex snyx.RWMutex
func getSum() int {
// 獲取讀寫(xiě)鎖
mutex.RLock()
defer mutex.RUnlock()
b := sum
return b
}
// 這樣性能會(huì)有很大提升宇攻,因?yàn)槎鄠€(gè)goroutine可以同時(shí)讀取數(shù)據(jù)惫叛,不再相互等待。
sync.WaitGroup
我們上述的示例中使用了time.Sleep函數(shù)尺碰, 是為了防止主函數(shù)main() 返回挣棕,一旦main函數(shù)范湖了, 程序也就退出了亲桥。 **提示**:一個(gè)函數(shù)或方法的返回(return) 也就意味著當(dāng)前函數(shù)執(zhí)行完畢洛心。
如我們我們執(zhí)行100個(gè)add協(xié)程和10個(gè)getSum協(xié)程,不知道什么時(shí)候執(zhí)行完畢题篷,所以設(shè)置了比較長(zhǎng)的等待時(shí)間词身,但是存在一個(gè)問(wèn)題,如果這110個(gè)協(xié)程很快就執(zhí)行完畢番枚,main函數(shù)應(yīng)該提前返回法严,但是還要等待一會(huì)才能返回,會(huì)產(chǎn)生性能問(wèn)題葫笼。但是如果等待時(shí)間截止時(shí)深啤,協(xié)程沒(méi)有執(zhí)行完畢,程序會(huì)提前退出路星,導(dǎo)致有協(xié)程沒(méi)有執(zhí)行完溯街,產(chǎn)生不可預(yù)知的結(jié)果。
如何解決這個(gè)問(wèn)題呢洋丐? 有沒(méi)有辦法監(jiān)聽(tīng)所有協(xié)程的執(zhí)行呈昔,一旦全部執(zhí)行完畢,程序馬上退出友绝,這樣既可保證所有協(xié)程執(zhí)行完畢堤尾,又可以及時(shí)退出節(jié)省時(shí)間,提升性能迁客。Channel可以解決這個(gè)問(wèn)題郭宝,不過(guò)很復(fù)雜辞槐。Go語(yǔ)言提供了更簡(jiǎn)潔的方法,它就是 **sync.WaitGroup**剩蟀。
func run() {
var wg sync.WaitGroup
wg.Add(110)
for i := 0; i< 100; i ++ {
go func() {
// 計(jì)數(shù)器減1
defer wg.Done()
add(10)
}()
}
for i := 1; i < 10 ; i++ {
go func() {
defer wg.Done()
fmt.Println("sum is", getSum())
}()
}
// 一直等待催蝗,直到計(jì)數(shù)器值為0
wg.Wait()
}
sync.WaitGroup使用比較簡(jiǎn)單,一共分為三步:
- 聲明一個(gè)sync.WaitGroup育特,然后通過(guò)Add方法設(shè)置計(jì)數(shù)器的值丙号,需要跟蹤多少個(gè)協(xié)程就設(shè)置多少
- 在每個(gè)協(xié)程執(zhí)行完畢時(shí)調(diào)用Done方法,讓計(jì)數(shù)器減1缰冤,告訴sync.WaiGroup 該協(xié)程已經(jīng)執(zhí)行完畢
- 最后調(diào)用Wait方法一直等待犬缨,直到計(jì)數(shù)器值為0,也就是所有跟蹤的協(xié)程都執(zhí)行完畢
通過(guò)sync.WaitGroup可以很好地跟蹤協(xié)程棉浸。在協(xié)程執(zhí)行完畢后怀薛,整個(gè)函數(shù)才會(huì)執(zhí)行完畢,時(shí)間不多不少迷郑,正好是協(xié)程執(zhí)行的時(shí)間枝恋。
sync.WaitGroup適合協(xié)調(diào)多個(gè)協(xié)程共同做一件事情的場(chǎng)景,比如下載一個(gè)文件嗡害,假設(shè)使用10個(gè)協(xié)程焚碌,每個(gè)協(xié)程寫(xiě)在1/10,只有10個(gè)協(xié)程都下載好了整個(gè)文件才下載好了霸妹。這就是我們經(jīng)常聽(tīng)說(shuō)的多線程下載十电,通過(guò)多個(gè)線程共同做一件事情,顯著提高效率叹螟。==小提示==我們可以把Go語(yǔ)言的協(xié)程理解為平常說(shuō)的線程鹃骂,從用戶體驗(yàn)上并無(wú)不可,但是從技術(shù)實(shí)現(xiàn)上罢绽,它們是不一樣的畏线。
sync.Once
在實(shí)際工作中,可能會(huì)有這樣的需求:讓代碼只執(zhí)行一次良价,哪怕是高并發(fā)情況下寝殴,比如創(chuàng)建一個(gè)單例。針對(duì)這種情形棚壁,Go語(yǔ)言為我們提供了sync.Once來(lái)保證代碼只執(zhí)行一次。
func main() {
doOnce()
}
func doOnce() {
var once sync.Once
onceBody := func() {
fmt.Println("Only once")
}
// 用于等待協(xié)程執(zhí)行完畢
done := make(chan bool)
// 啟動(dòng)10個(gè)協(xié)程執(zhí)行once.Do(onceBody)
for i := 1; i < 10; i++ {
go func(){
go func() {
// 把要執(zhí)行的函數(shù)(方法)作為參數(shù)傳給once.Do方法即可
once.Do(onceBody)
done <- true
}
}()
}
for i := 10; i < 10 ; i++{
<- done
}
}
這是Go語(yǔ)言自帶的一個(gè)示例栈虚,雖然啟動(dòng)了10個(gè)協(xié)程來(lái)執(zhí)行onceBody函數(shù)袖外,但是因?yàn)橛昧薿nce.Do方法,所以函數(shù)onceBody只會(huì)執(zhí)行一次魂务。也就是在高并發(fā)的情況下曼验,sync.Once也會(huì)保證onceBody函數(shù)只執(zhí)行一次泌射。
sync.Once 適用于創(chuàng)建某個(gè)對(duì)象的單例、只加載一次的資源等只執(zhí)行一次的場(chǎng)景鬓照。
sync.Cond
在Go語(yǔ)言中熔酷,sync.WaitGroup用于最終完成的場(chǎng)景,關(guān)鍵點(diǎn)在于一定要等待所有協(xié)程都執(zhí)行完畢豺裆。而**sync.Cond 可以用于發(fā)號(hào)施令**拒秘,一聲令下所有協(xié)程都可以開(kāi)始執(zhí)行,關(guān)鍵點(diǎn)在于協(xié)程開(kāi)始的時(shí)候是等待的臭猜,要等待sync.Cond喚醒才能執(zhí)行躺酒。
sync.Cond從字面意思看是條件變量,它具有阻塞協(xié)程和喚醒協(xié)程的功能蔑歌,所有可以在滿足一定條件的情況下喚醒協(xié)程羹应,但條件變量只是它的一種使用場(chǎng)景。
// 10個(gè)人賽跑次屠,1人裁判發(fā)號(hào)施令
func race() {
cond := sync.NewCond(&sync.Mutex{})
var wg sync.WaitGroup
wg.Add(11)
for i := 1; i < 10; i++ {
go func(num int) {
defer wg.Done()
fmt.Println(num, "號(hào)已就位")
cond.L.Lock()
cond.Wait() // 等待發(fā)令槍響
fmt.Println(num, "號(hào)開(kāi)始跑")
cond.L.Unlock()
}(i)
}
time.Sleep(2 * time.Second)
go func(){
defer wg.Done()
fmt.Println("裁判已就位园匹,準(zhǔn)備發(fā)令槍")
fmt.Println("比賽開(kāi)始,大家開(kāi)始跑")
cond.Broadcast() // 發(fā)令槍響
}()
wg.wait()
}
大概步驟:
- 通過(guò)sync.NewCond函數(shù)生成一個(gè)*sync.Cond劫灶,用于阻塞和喚醒協(xié)程
- 然后啟動(dòng)10個(gè)協(xié)程模擬10個(gè)人裸违,準(zhǔn)備就位后調(diào)用cond.Wait()方法阻塞當(dāng)前協(xié)程等待發(fā)令槍響,這里需要注意的是調(diào)用cond.Wait()方法是要加鎖
- time.Sleep 用于等待所有人都進(jìn)入wait狀態(tài)浑此,這樣裁判才能調(diào)用cond.Broadcast()發(fā)號(hào)施令
- 裁判準(zhǔn)備完畢后累颂,可以調(diào)用cond.Broadcast通知所有人開(kāi)始跑了
sync.Cond 有三個(gè)方法,分別是:
- Wait凛俱,阻塞當(dāng)前協(xié)程紊馏,知道被其他協(xié)程調(diào)用Broadcast 或者 Signal 方法喚醒,使用的時(shí)候需要加鎖蒲犬,使用sync.Cond中的鎖即可朱监,也就是L字段
- **Signal **,喚醒一個(gè)等待時(shí)間最長(zhǎng)的協(xié)程
- Broadcast原叮,喚醒所有等待的協(xié)程
注意:在調(diào)用signal 或者Broadcast之前赫编,要確保目標(biāo)協(xié)程處于Wait阻塞狀態(tài),不然會(huì)出現(xiàn)死鎖問(wèn)題奋隶。
小結(jié):我們了解了Go語(yǔ)言的同步原語(yǔ)使用擂送,通過(guò)它們可以更靈活的控制多協(xié)程的并發(fā)。從使用上講唯欣,Go語(yǔ)言還是更推薦channel這種更高級(jí)別的并發(fā)控制方式嘹吨,因?yàn)樗?jiǎn)潔,也更容易理解和使用境氢。同步原語(yǔ)通常用于更復(fù)雜的并發(fā)控制蟀拷,如果追求更靈活的控制方式和性能碰纬,可以使用它們。