本篇文章面向的讀者: 已經(jīng)基本掌握Go中的 協(xié)程(goroutine),通道(channel),互斥鎖(sync.Mutex)絮蒿,讀寫鎖(sync.RWMutex) 這些知識(shí)尊搬。如果對(duì)這些還不太懂,可以先回去把這幾個(gè)知識(shí)點(diǎn)解決了土涝。
首先理解以下三點(diǎn)再進(jìn)入正題:
- Go中的一個(gè)協(xié)程 可以理解成一個(gè)獨(dú)立的人佛寿,多個(gè)協(xié)程是多個(gè)獨(dú)立的人
- 多個(gè)協(xié)程都需要訪問(wèn)的 共享資源(比如共享變量) 可以理解成 多人要用的某種公共社會(huì)資源
- 上鎖 其實(shí) 就是加入到 某個(gè)共享資源的 爭(zhēng)搶組中;上鎖完成 就是 從爭(zhēng)搶組中被選出但壮,得到了期待的共享資源冀泻;解鎖 就是退出 某個(gè)共享資源的 爭(zhēng)搶組 。
假如有這樣一個(gè)現(xiàn)實(shí)場(chǎng)景:在一個(gè)公園中有一個(gè)公共廁所蜡饵,這個(gè)廁所一次只能容納一個(gè)人上廁所弹渔,同時(shí)這個(gè)廁所中有個(gè)放卷紙的位置,其一次只能放一卷紙溯祸,一卷紙的總長(zhǎng)度是 5 米肢专,而每個(gè)人上一次廁所需要用掉 1 米的紙。而當(dāng)一卷紙用完后焦辅,公園管理員要負(fù)責(zé)給廁所加上一卷新紙博杖,以便大家可以繼續(xù)使用廁所。 那么對(duì)于這個(gè)單人公共廁所筷登,大家只能排隊(duì)上廁所剃根,當(dāng)每個(gè)人進(jìn)到廁所的時(shí)候,當(dāng)然會(huì)把廁所門鎖好前方,以便任何人都進(jìn)不來(lái)(包括管理員)狈醉。管理員若要進(jìn)到廁所查看用紙情況并加卷紙,也需要排隊(duì)(因?yàn)椴尻?duì)總是不文明對(duì)吧)镣丑。
那么怎么用 Golang 去模擬上述場(chǎng)景呢舔糖?
首先我們先不用 sync.Cond娱两,看如何實(shí)現(xiàn)莺匠?那么請(qǐng)看下面這段代碼:
package main
import (
"fmt"
"time"
"sync"
)
var 卷紙 int
var m sync.Mutex
var wg sync.WaitGroup
func 上廁所(姓名 string){
m.Lock()
defer func(){
m.Unlock()
wg.Done()
}()
fmt.Printf("%s 進(jìn)到廁所\t",姓名)
if 卷紙 >= 1 { // 進(jìn)到廁所第一件事是看還有沒(méi)有紙
fmt.Printf("正在拉屎中...\n")
time.Sleep(time.Second)
卷紙 -= 1
fmt.Printf("%s 已用完廁所,正在離開\n",姓名)
return
}
fmt.Printf("發(fā)現(xiàn)紙用完了十兢,無(wú)奈先離開廁所\n")
}
func 加廁紙(){
m.Lock()
defer func(){
m.Unlock()
wg.Done()
}()
fmt.Printf("公園管理員 進(jìn)到廁所\t")
if 卷紙 <= 0 { // 管理員進(jìn)到廁所是看紙有沒(méi)有用完
fmt.Printf("公園管理員 正在加新紙...\n")
time.Sleep(time.Millisecond*500)
卷紙 = 5
fmt.Printf("公園管理員 已加上新廁紙趣竣,正在離開\n")
}else{
fmt.Printf("發(fā)現(xiàn)紙還沒(méi)用完,先離開廁所\n")
}
}
func main() {
卷紙 = 5 // 廁所一開始就準(zhǔn)備好了一卷紙旱物,長(zhǎng)度5米
要排隊(duì)上廁所的人 := [...]string{"老王","小李","老張","小劉","阿明","欣欣","西西","芳芳"}
for _,誰(shuí) := range 要排隊(duì)上廁所的人 {
wg.Add(1)
go 上廁所(誰(shuí))
}
wg.Add(1)
go 加廁紙()
wg.Wait()
}
/*
輸出(由于協(xié)程執(zhí)行順序的不可預(yù)測(cè)性遥缕,因此每次輸出的順序都可能不一樣):
公園管理員 進(jìn)到廁所 發(fā)現(xiàn)紙還沒(méi)用完,先離開廁所
阿明 進(jìn)到廁所 正在拉屎中...
阿明 已用完廁所宵呛,正在離開
老王 進(jìn)到廁所 正在拉屎中...
老王 已用完廁所单匣,正在離開
小劉 進(jìn)到廁所 正在拉屎中...
小劉 已用完廁所,正在離開
小李 進(jìn)到廁所 正在拉屎中...
小李 已用完廁所,正在離開
老張 進(jìn)到廁所 正在拉屎中...
老張 已用完廁所户秤,正在離開
欣欣 進(jìn)到廁所 發(fā)現(xiàn)紙用完了码秉,無(wú)奈先離開廁所
芳芳 進(jìn)到廁所 發(fā)現(xiàn)紙用完了,無(wú)奈先離開廁所
西西 進(jìn)到廁所 發(fā)現(xiàn)紙用完了鸡号,無(wú)奈先離開廁所
*/
上面的代碼已經(jīng)能看出一些效果转砖,但還是有問(wèn)題:最后三個(gè)人因?yàn)閹堄猛辏贾苯与x開廁所后就沒(méi)有后續(xù)了鲸伴?應(yīng)該是他們離開廁所后再次嘗試排隊(duì)府蔗,直到需求解決,就離開廁所不再參與排隊(duì)了汞窗,否則要不斷去排隊(duì)上廁所姓赤。而公園管理員呢,他要一直去排隊(duì)進(jìn)到廁所里看還有沒(méi)有紙仲吏,而不是看一次就再也不管了模捂。 那么請(qǐng)看下面的完善代碼:
package main
import (
"fmt"
"sync"
"time"
)
var (
卷紙 int
m sync.Mutex
wg sync.WaitGroup
廁所的排隊(duì) chan string
)
func 上廁所(姓名 string) {
m.Lock() // 該語(yǔ)句的調(diào)用只說(shuō)明本執(zhí)行體(可理解成該姓名所指的那個(gè)人)加入到了廁所資源的爭(zhēng)搶組中;
// 而該語(yǔ)句的完成調(diào)用蜘矢,才代表了從爭(zhēng)搶組中脫穎而出狂男,搶到了廁所;在完成調(diào)用之前品腹,會(huì)一直阻塞在這里(可理解為這個(gè)人正在爭(zhēng)搶中)
defer func() {
m.Unlock()
wg.Done()
}()
fmt.Printf("%s 進(jìn)到廁所\t", 姓名)
if 卷紙 >= 1 { // 進(jìn)到廁所第一件事是看還有沒(méi)有紙
fmt.Printf("正在拉屎中...\n")
time.Sleep(time.Second)
卷紙 -= 1
fmt.Printf("%s 已用完廁所岖食,正在離開\n", 姓名)
return
}
fmt.Printf("發(fā)現(xiàn)紙用完了,無(wú)奈先離開廁所\n")
廁所的排隊(duì) <- 姓名 // 再次加入廁所排隊(duì)舞吭,期望下次可以成功如廁
}
func 加廁紙() {
m.Lock()
defer m.Unlock()
fmt.Printf("公園管理員 進(jìn)到廁所\t")
if 卷紙 <= 0 { // 管理員進(jìn)到廁所是看紙有沒(méi)有用完
fmt.Printf("公園管理員 正在加新紙...\n")
time.Sleep(time.Millisecond * 500)
卷紙 = 5
fmt.Printf("公園管理員 已加上新廁紙泡垃,正在離開\n")
} else {
fmt.Printf("發(fā)現(xiàn)紙還沒(méi)用完,先離開廁所\n")
}
}
func main() {
卷紙 = 5 // 廁所一開始就準(zhǔn)備好了一卷紙羡鸥,長(zhǎng)度5米
要上廁所的人 := [...]string{"老王", "小李", "老張", "小劉", "阿明", "欣欣", "西西", "芳芳"} // 這里只是舉幾個(gè)人名例子蔑穴,假設(shè)此處有源源不斷的人去上廁所(讀者可以隨意改造人名來(lái)源)
廁所的排隊(duì) = make(chan string, len(要上廁所的人))
for _, 誰(shuí) := range 要上廁所的人 {
廁所的排隊(duì) <- 誰(shuí)
}
go func() { // 在這個(gè)執(zhí)行體中,會(huì)不斷從 廁所排隊(duì) 中把人加入到 對(duì)廁所資源的爭(zhēng)搶組中
for 誰(shuí) := range 廁所的排隊(duì) {
wg.Add(1)
go 上廁所(誰(shuí))
}
}()
wg.Add(1)
go func() { // 在這個(gè)執(zhí)行體中惧浴,代表公園管理員的個(gè)人時(shí)間線存和,他會(huì)每隔一段時(shí)間去加入爭(zhēng)搶組進(jìn)到廁所,檢查紙還有沒(méi)有
for {
time.Sleep(time.Millisecond * 1200)
加廁紙()
}
}()
wg.Wait()
}
/*
輸出:
老王 進(jìn)到廁所 正在拉屎中...
老王 已用完廁所衷旅,正在離開
芳芳 進(jìn)到廁所 正在拉屎中...
芳芳 已用完廁所捐腿,正在離開
阿明 進(jìn)到廁所 正在拉屎中...
阿明 已用完廁所,正在離開
小劉 進(jìn)到廁所 正在拉屎中...
小劉 已用完廁所柿顶,正在離開
欣欣 進(jìn)到廁所 正在拉屎中...
欣欣 已用完廁所茄袖,正在離開
小李 進(jìn)到廁所 發(fā)現(xiàn)紙用完了,無(wú)奈先離開廁所
老張 進(jìn)到廁所 發(fā)現(xiàn)紙用完了嘁锯,無(wú)奈先離開廁所
西西 進(jìn)到廁所 發(fā)現(xiàn)紙用完了宪祥,無(wú)奈先離開廁所
公園管理員 進(jìn)到廁所 公園管理員 正在加新紙...
公園管理員 已加上新廁紙聂薪,正在離開
西西 進(jìn)到廁所 正在拉屎中...
西西 已用完廁所,正在離開
小李 進(jìn)到廁所 正在拉屎中...
小李 已用完廁所蝗羊,正在離開
老張 進(jìn)到廁所 正在拉屎中...
老張 已用完廁所胆建,正在離開
公園管理員 進(jìn)到廁所 發(fā)現(xiàn)紙還沒(méi)用完,先離開廁所
公園管理員 進(jìn)到廁所 發(fā)現(xiàn)紙還沒(méi)用完肘交,先離開廁所
公園管理員 進(jìn)到廁所 發(fā)現(xiàn)紙還沒(méi)用完笆载,先離開廁所
*/
上面這個(gè)代碼在功能上基本是完善了,成功模擬了上述 多人上公廁 的場(chǎng)景涯呻。但仔細(xì)一想凉驻,這個(gè)場(chǎng)景其實(shí)有些地方是不合常理的:如果有個(gè)人進(jìn)到廁所發(fā)現(xiàn)沒(méi)紙,難道他會(huì)出來(lái)緊接著再去排隊(duì)嗎复罐?如果排了三次五次甚至十次還是沒(méi)有紙涝登,還要這樣不斷地反復(fù)排隊(duì)進(jìn)去出來(lái)又排隊(duì)?而公園管理員效诅,要是這樣不斷反復(fù)排隊(duì)進(jìn)廁所查看胀滚,那么他這一天其他啥事都干不了。
所以更合理實(shí)際的情況應(yīng)該是:如果一個(gè)人進(jìn)到廁所發(fā)現(xiàn)沒(méi)紙乱投,他應(yīng)該先去在旁邊歇著或在附近干別的咽笼,當(dāng)公園管理員加完紙后,會(huì)通過(guò)喇叭吆喝一聲:“新紙已加上”戚炫。這樣剑刑,附近所有因?yàn)闆](méi)廁紙而歇著的人就會(huì)聽到這個(gè)通知,此時(shí)双肤,他們?cè)偃L試排隊(duì)進(jìn)廁所施掏;而公園管理員也不用不斷去排隊(duì)進(jìn)廁所檢查紙用完了沒(méi)有,因?yàn)榻?jīng)過(guò)升級(jí)茅糜,廁所加裝了一個(gè)功能七芭,有一個(gè)紙用盡的報(bào)警按鈕裝在紙盒旁邊,當(dāng)上完廁所的人發(fā)現(xiàn)紙用完的時(shí)候蔑赘,他會(huì)先按下這個(gè)報(bào)警按鈕狸驳,再離開廁所。這個(gè)報(bào)警的聲音在整個(gè)公園的各處都可以聽到米死,所以管理員無(wú)論在哪里干啥锌历,他都能收到這個(gè)紙用盡的報(bào)警信號(hào),然后他才去進(jìn)廁所加紙峦筒。
其實(shí)這種被動(dòng)通知的模式就是 sync.Cond 的核心思想,它會(huì)減少資源消耗窗慎,達(dá)到更優(yōu)的效果物喷,下面就是改良為 sync.Cond 的實(shí)現(xiàn)代碼:
package main
import (
"fmt"
"math"
"strconv"
"sync"
"time"
)
var (
卷紙 int
m sync.Mutex
cond = sync.NewCond(&m)
)
func 上廁所(姓名 string) {
m.Lock() // 該語(yǔ)句的調(diào)用只說(shuō)明本執(zhí)行體(可理解成該姓名所指的那個(gè)人)加入到了廁所資源的爭(zhēng)搶組中卤材;
// 而該語(yǔ)句的完成調(diào)用,才代表了從爭(zhēng)搶組中脫穎而出峦失,搶到了廁所扇丛;在完成調(diào)用之前,會(huì)一直阻塞在這里(可理解為這個(gè)人正在爭(zhēng)搶中)
defer m.Unlock()
fmt.Printf("%s 進(jìn)到廁所\t", 姓名)
for 卷紙 < 1 { // 進(jìn)到廁所第一件事是看還有沒(méi)有紙
fmt.Printf("發(fā)現(xiàn)紙用完了尉辑,先離開廁所在附近歇息等待信號(hào)\n")
cond.Wait() // 該語(yǔ)句的調(diào)用 相當(dāng)于調(diào)用了 m.Unlock() 也就是退出了爭(zhēng)搶組帆精,而是先歇著等待紙加上的信號(hào);
// 當(dāng)收到紙加上的信號(hào)后隧魄,該語(yǔ)句會(huì)自動(dòng)執(zhí)行 m.Lock()卓练,也就是會(huì)重新加入到廁所的爭(zhēng)搶組中;
// 該語(yǔ)句的完成調(diào)用說(shuō)明已經(jīng)再次成功爭(zhēng)搶到了廁所购啄;
fmt.Printf("%s 等到了廁紙已加的信號(hào)襟企,并去再次搶到了廁所\t", 姓名)
}
fmt.Printf("正在拉屎中...\n")
time.Sleep(time.Second)
卷紙 -= 1
fmt.Printf("%s 已用完廁所\t", 姓名)
if 卷紙 < 1 { // 注意這里:在他用完廁所離開前,他需要看是不是紙已經(jīng)用完了狮含,如果用完了顽悼,就按下紙用盡的報(bào)警按鈕,給公園管理員發(fā)送信號(hào)
cond.Broadcast() // 想想几迄,這里為什么不用 Signal() 蔚龙?因?yàn)?Signal 只能通知到一個(gè)等待者,這樣就有可能通知不到 公園管理員映胁「撸可以試著把這里換成 Signal() 試下
fmt.Printf("發(fā)現(xiàn)廁紙已用完,并按下了報(bào)警\t")
}
fmt.Printf("正在離開廁所\n")
}
func 加廁紙() {
m.Lock()
defer m.Unlock()
fmt.Printf("公園管理員 進(jìn)到廁所\t")
for 卷紙 > 0 { // 管理員進(jìn)到廁所是看紙有沒(méi)有用完
fmt.Printf("發(fā)現(xiàn)紙還沒(méi)用完屿愚,先離開廁所在等紙用盡的報(bào)警消息\n")
cond.Wait() // 如果紙沒(méi)用完汇跨,就先去干其他工作,等紙用盡的報(bào)警消息
fmt.Printf("公園管理員 等到了紙用盡的報(bào)警消息妆距,并再次搶到了廁所\n")
}
fmt.Printf("公園管理員 正在加新紙...\n")
time.Sleep(time.Millisecond * 500)
卷紙 = 5
cond.Broadcast() // 注意:公園管理員加完新紙后穷遂,要通過(guò)喇叭喊一聲 “紙已加上” 的消息通知所有 因沒(méi)紙而等待上廁所的人
fmt.Printf("公園管理員 已加上新廁紙,并通過(guò)喇叭通知了該消息娱据,并正在離開廁所\n")
}
func main() {
卷紙 = 5 // 廁所一開始就準(zhǔn)備好了一卷紙蚪黑,長(zhǎng)度5米
要上廁所的人 := [...]string{"老王", "小李", "老張", "小劉", "阿明", "欣欣", "西西", "芳芳"} // 上廁所的人名模板
go func() { // 在這個(gè)執(zhí)行體中,代表廁所及廁所隊(duì)列的時(shí)間線中剩,廁所永遠(yuǎn)運(yùn)營(yíng)下去
for i := 0; i < math.MaxInt; i++ { // 此循環(huán)通過(guò)編號(hào)加上上面的姓名模板來(lái) 創(chuàng)建源源不斷 上廁所的人
for _, 人名模板 := range 要上廁所的人 {
誰(shuí) := 人名模板 + strconv.Itoa(i)
go 上廁所(誰(shuí))
time.Sleep(time.Millisecond * 500) // 平均每半秒有一個(gè)人去上廁所
}
fmt.Printf("\n====================>> 屏幕停止輸出后忌穿,請(qǐng)按Enter鍵繼續(xù) <<====================\n\n")
fmt.Scanln()
}
}()
go func() { // 在這個(gè)執(zhí)行體中,代表公園管理員的個(gè)人時(shí)間線结啼,管理員永不退休
for {
// 注意:相比上個(gè)版本掠剑,此處不用再加 Sleep 函數(shù)了,因?yàn)?加廁紙() 函數(shù)中的 cond.Wait() 會(huì)在有紙的時(shí)候等待信號(hào)
加廁紙()
}
}()
end := make(chan bool)
<-end
}
/*
輸出:
公園管理員 進(jìn)到廁所 發(fā)現(xiàn)紙還沒(méi)用完郊愧,先離開廁所在等紙用盡的報(bào)警消息
老王0 進(jìn)到廁所 正在拉屎中...
老王0 已用完廁所 正在離開廁所
小李0 進(jìn)到廁所 正在拉屎中...
小李0 已用完廁所 正在離開廁所
老張0 進(jìn)到廁所 正在拉屎中...
老張0 已用完廁所 正在離開廁所
小劉0 進(jìn)到廁所 正在拉屎中...
小劉0 已用完廁所 正在離開廁所
阿明0 進(jìn)到廁所 正在拉屎中...
====================>> 屏幕停止輸出后朴译,請(qǐng)按Enter鍵繼續(xù) <<====================
阿明0 已用完廁所 發(fā)現(xiàn)廁紙已用完井佑,并按下了報(bào)警 正在離開廁所
欣欣0 進(jìn)到廁所 發(fā)現(xiàn)紙用完了忠蝗,先離開廁所在附近歇息等待信號(hào)
西西0 進(jìn)到廁所 發(fā)現(xiàn)紙用完了黍瞧,先離開廁所在附近歇息等待信號(hào)
芳芳0 進(jìn)到廁所 發(fā)現(xiàn)紙用完了,先離開廁所在附近歇息等待信號(hào)
公園管理員 等到了紙用盡的報(bào)警消息查乒,并再次搶到了廁所
公園管理員 正在加新紙...
公園管理員 已加上新廁紙盯拱,并通過(guò)喇叭通知了該消息盒发,并正在離開廁所
公園管理員 進(jìn)到廁所 發(fā)現(xiàn)紙還沒(méi)用完,先離開廁所在等紙用盡的報(bào)警消息
欣欣0 等到了廁紙已加的信號(hào)狡逢,并去再次搶到了廁所 正在拉屎中...
欣欣0 已用完廁所 正在離開廁所
芳芳0 等到了廁紙已加的信號(hào)宁舰,并去再次搶到了廁所 正在拉屎中...
芳芳0 已用完廁所 正在離開廁所
西西0 等到了廁紙已加的信號(hào),并去再次搶到了廁所 正在拉屎中...
西西0 已用完廁所 正在離開廁所
老王1 進(jìn)到廁所 正在拉屎中...
老王1 已用完廁所 正在離開廁所
小李1 進(jìn)到廁所 正在拉屎中...
小李1 已用完廁所 發(fā)現(xiàn)廁紙已用完甚侣,并按下了報(bào)警 正在離開廁所
老張1 進(jìn)到廁所 發(fā)現(xiàn)紙用完了明吩,先離開廁所在附近歇息等待信號(hào)
公園管理員 等到了紙用盡的報(bào)警消息,并再次搶到了廁所
公園管理員 正在加新紙...
公園管理員 已加上新廁紙殷费,并通過(guò)喇叭通知了該消息印荔,并正在離開廁所
公園管理員 進(jìn)到廁所 發(fā)現(xiàn)紙還沒(méi)用完,先離開廁所在等紙用盡的報(bào)警消息
小劉1 進(jìn)到廁所 正在拉屎中...
小劉1 已用完廁所 正在離開廁所
阿明1 進(jìn)到廁所 正在拉屎中...
====================>> 屏幕停止輸出后详羡,請(qǐng)按Enter鍵繼續(xù) <<====================
*/
用了 sync.Cond 的代碼顯然要精簡(jiǎn)了很多仍律,而且還節(jié)省了計(jì)算資源,只會(huì)在收到通知的時(shí)候 才去搶公共廁所实柠,而不是不斷地反復(fù)去搶公共廁所水泉。通過(guò)這個(gè)對(duì)現(xiàn)實(shí)場(chǎng)景的模擬,我們就很容易從使用者的角度理解 sync.Cond 是什么窒盐,它的字面意思就是 “條件”草则,這就已經(jīng)點(diǎn)出了這東西的核心要義,就是滿足條件才執(zhí)行蟹漓,條件是什么炕横,信號(hào)其實(shí)就是條件,當(dāng)一個(gè)執(zhí)行體收到信號(hào)之后葡粒,它才去爭(zhēng)搶共享資源份殿,否則就會(huì)掛起等待(這種等待底層其實(shí)會(huì)讓出線程,所以這種等待并不會(huì)空耗資源)嗽交,比起不斷輪尋去搶資源卿嘲,這種方式要節(jié)省得多。
最后留給讀者一個(gè)思考的問(wèn)題:就是上面最后一版的代碼夫壁,為什么 當(dāng)紙用完后按報(bào)警按鈕通知 公園管理員 要用 sync.Broadcast() 方法去廣播通知拾枣?不是只通知管理員一個(gè)人嗎,單獨(dú)通知他不就行了掌唾,用 sync.Signal() 為什么不行放前?