【Go同步原語(yǔ)】

在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)單,一共分為三步:

  1. 聲明一個(gè)sync.WaitGroup育特,然后通過(guò)Add方法設(shè)置計(jì)數(shù)器的值丙号,需要跟蹤多少個(gè)協(xié)程就設(shè)置多少
  2. 在每個(gè)協(xié)程執(zhí)行完畢時(shí)調(diào)用Done方法,讓計(jì)數(shù)器減1缰冤,告訴sync.WaiGroup 該協(xié)程已經(jīng)執(zhí)行完畢
  3. 最后調(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()
}

大概步驟:

  1. 通過(guò)sync.NewCond函數(shù)生成一個(gè)*sync.Cond劫灶,用于阻塞和喚醒協(xié)程
  2. 然后啟動(dòng)10個(gè)協(xié)程模擬10個(gè)人裸违,準(zhǔn)備就位后調(diào)用cond.Wait()方法阻塞當(dāng)前協(xié)程等待發(fā)令槍響,這里需要注意的是調(diào)用cond.Wait()方法是要加鎖
  3. time.Sleep 用于等待所有人都進(jìn)入wait狀態(tài)浑此,這樣裁判才能調(diào)用cond.Broadcast()發(fā)號(hào)施令
  4. 裁判準(zhǔn)備完畢后累颂,可以調(diào)用cond.Broadcast通知所有人開(kāi)始跑了

sync.Cond 有三個(gè)方法,分別是:

  1. Wait凛俱,阻塞當(dāng)前協(xié)程紊馏,知道被其他協(xié)程調(diào)用Broadcast 或者 Signal 方法喚醒,使用的時(shí)候需要加鎖蒲犬,使用sync.Cond中的鎖即可朱监,也就是L字段
  2. **Signal **,喚醒一個(gè)等待時(shí)間最長(zhǎng)的協(xié)程
  3. 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ā)控制蟀拷,如果追求更靈活的控制方式和性能碰纬,可以使用它們。


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末问芬,一起剝皮案震驚了整個(gè)濱河市悦析,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌此衅,老刑警劉巖强戴,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異炕柔,居然都是意外死亡酌泰,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)匕累,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)陵刹,“玉大人,你說(shuō)我怎么就攤上這事欢嘿∷ニ觯” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵炼蹦,是天一觀的道長(zhǎng)羡宙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)掐隐,這世上最難降的妖魔是什么狗热? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮虑省,結(jié)果婚禮上匿刮,老公的妹妹穿的比我還像新娘。我一直安慰自己探颈,他們只是感情好熟丸,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著伪节,像睡著了一般光羞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怀大,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天纱兑,我揣著相機(jī)與錄音,去河邊找鬼化借。 笑死潜慎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播勘纯,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钓瞭!你這毒婦竟也來(lái)了驳遵?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤山涡,失蹤者是張志新(化名)和其女友劉穎堤结,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鸭丛,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡竞穷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鳞溉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘾带。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖熟菲,靈堂內(nèi)的尸體忽然破棺而出看政,到底是詐尸還是另有隱情,我是刑警寧澤抄罕,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布允蚣,位于F島的核電站,受9級(jí)特大地震影響呆贿,放射性物質(zhì)發(fā)生泄漏嚷兔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一做入、第九天 我趴在偏房一處隱蔽的房頂上張望冒晰。 院中可真熱鬧,春花似錦母蛛、人聲如沸翩剪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)前弯。三九已至,卻和暖如春秫逝,著一層夾襖步出監(jiān)牢的瞬間恕出,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工违帆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浙巫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像的畴,于是被迫代替她去往敵國(guó)和親渊抄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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

  • 本文將講解一下Go語(yǔ)言中的同步原語(yǔ)與鎖。會(huì)闡述幾種常見(jiàn)的鎖煎娇,剖析其流程二庵,然后針對(duì)每種同步原語(yǔ)舉幾個(gè)例子。由于文章比...
    鏈人成長(zhǎng)chainerup閱讀 657評(píng)論 0 0
  • 官方包的注釋?zhuān)?sync包提供基礎(chǔ)的同步原語(yǔ)缓呛,sync.Mutext催享、sync.RWMutex、sync.Wait...
    thepoy閱讀 596評(píng)論 0 1
  • 本文從上下文Context哟绊、同步原語(yǔ)與鎖因妙、Channel、調(diào)度器四個(gè)方面介紹Go語(yǔ)言是如何實(shí)現(xiàn)并發(fā)的票髓。本文絕大部分...
    彥幀閱讀 1,558評(píng)論 1 3
  • sync.Mutex sync.Mutex 即為互斥鎖兰迫。 注意:sync.Mutex 不支持遞歸鎖,加鎖再加鎖炬称,會(huì)...
    wayyyy閱讀 369評(píng)論 0 0
  • go并發(fā)編程入門(mén)到放棄 并發(fā)和并行 并發(fā):一個(gè)處理器同時(shí)處理多個(gè)任務(wù)汁果。 并行:多個(gè)處理器或者是多核的處理器同時(shí)處理...
    yangyunfeng閱讀 551評(píng)論 0 2