Golang并發(fā):除了channel馋袜,你還有其他選擇

我們都知道Golang并發(fā)優(yōu)選channel舶斧,但channel不是萬能的茴厉,Golang為我們提供了另一種選擇:sync。通過這篇文章怀酷,你會了解sync包最基礎(chǔ)蜕依、最常用的方法样眠,至于sync和channel之爭留給下一篇文章。

sync包提供了基礎(chǔ)的異步操作方法商佑,比如互斥鎖(Mutex)厢塘、單次執(zhí)行(Once)和等待組(WaitGroup)晚碾,這些異步操作主要是為低級庫提供格嘁,上層的異步/并發(fā)操作最好選用通道和通信糕簿。

sync包提供了:

  1. Mutex:互斥鎖
  2. RWMutex:讀寫鎖
  3. WaitGroup:等待組
  4. Once:單次執(zhí)行
  5. Cond:信號量
  6. Pool:臨時對象池
  7. Map:自帶鎖的map

這篇文章是sync包的入門文章,所以只介紹常用的結(jié)構(gòu)和方法:Mutex蜂嗽、RWMutex植旧、WaitGroup病附、Once,而Cond亥鬓、PoolMap留給大家自行探索完沪,或有需求再介紹。

互斥鎖

常做并發(fā)工作的朋友對互斥鎖應(yīng)該不陌生贮竟,Golang里互斥鎖需要確保的是某段時間內(nèi)丽焊,不能有多個協(xié)程同時訪問一段代碼(臨界區(qū))较剃。

互斥鎖被稱為Mutex咕别,它有2個函數(shù)技健,Lock()Unlock()分別是獲取鎖和釋放鎖,如下:

type Mutex
func (m *Mutex) Lock(){}
func (m *Mutex) Unlock(){}

Mutex的初始值為未鎖的狀態(tài)雌贱,并且Mutex通常作為結(jié)構(gòu)體的匿名成員存在

經(jīng)過了上面這么“官方”的介紹偿短,舉個例子:你在工商銀行有100元存款欣孤,這張卡綁定了支付寶和微信,在中午12點你用支付寶支付外賣30元昔逗,你在微信發(fā)紅包降传,搶到10塊。銀行需要按順序執(zhí)行上面兩件事勾怒,先減30再加10或者先加10再減30婆排,結(jié)果都是80,但如果同時執(zhí)行笔链,結(jié)果可能是段只,只減了30或者只加了10,即你有70元或者你有110元鉴扫。前一個結(jié)果是你賠了赞枕,后一個結(jié)果是銀行賠了,銀行可不希望把這種事算錯坪创。

看看實際使用吧:創(chuàng)建一個銀行炕婶,銀行里存每個賬戶的錢,存儲查詢都加了鎖操作莱预,這樣銀行就不會算錯賬了古话。
銀行的定義:

type Bank struct {
    sync.Mutex
    saving map[string]int // 每賬戶的存款金額
}

func NewBank() *Bank {
    b := &Bank{
        saving: make(map[string]int),
    }
    return b
}

銀行的存取錢:

// Deposit 存款
func (b *Bank) Deposit(name string, amount int) {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        b.saving[name] = 0
    }
    b.saving[name] += amount
}

// Withdraw 取款,返回實際取到的金額
func (b *Bank) Withdraw(name string, amount int) int {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }
    if b.saving[name] < amount {
        amount = b.saving[name]
    }
    b.saving[name] -= amount

    return amount
}

// Query 查詢余額
func (b *Bank) Query(name string) int {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }

    return b.saving[name]
}

模擬操作:小米支付寶存了100锁施,并且同時花了20陪踩。

func main() {
    b := NewBank()
    go b.Deposit("xiaoming", 100)
    go b.Withdraw("xiaoming", 20)
    go b.Deposit("xiaogang", 2000)

    time.Sleep(time.Second)
    fmt.Printf("xiaoming has: %d\n", b.Query("xiaoming"))
    fmt.Printf("xiaogang has: %d\n", b.Query("xiaogang"))
}

結(jié)果:先存后花。

?  sync_pkg git:(master) ? go run mutex.go
xiaoming has: 80
xiaogang has: 2000

也可能是:先花后存悉抵,因為先花20肩狂,因為小明沒錢,所以沒花出去姥饰。

?  sync_pkg git:(master) ? go run mutex.go
xiaoming has: 100
xiaogang has: 2000

這個例子只是介紹了mutex的基本使用傻谁,如果你想多研究下mutex,那就去我的Github(閱讀原文)下載下來代碼列粪,自己修改測試审磁。Github中還提供了沒有鎖的例子谈飒,運行多次總能碰到錯誤:

fatal error: concurrent map writes
這是由于并發(fā)訪問map造成的。

讀寫鎖

讀寫鎖是互斥鎖的特殊變種态蒂,如果是計算機基本知識扎實的朋友會知道杭措,讀寫鎖來自于讀者和寫者的問題,這個問題就不介紹了钾恢,介紹下我們的重點:讀寫鎖要達(dá)到的效果是同一時間可以允許多個協(xié)程讀數(shù)據(jù)手素,但只能有且只有1個協(xié)程寫數(shù)據(jù)

也就是說瘩蚪,讀和寫是互斥的泉懦,寫和寫也是互斥的,但讀和讀并不互斥疹瘦。具體講崩哩,當(dāng)有至少1個協(xié)程讀時,如果需要進(jìn)行寫言沐,就必須等待所有已經(jīng)在讀的協(xié)程結(jié)束讀操作邓嘹,寫操作的協(xié)程才獲得鎖進(jìn)行寫數(shù)據(jù)。當(dāng)寫數(shù)據(jù)的協(xié)程已經(jīng)在進(jìn)行時呢灶,有其他協(xié)程需要進(jìn)行讀或者寫吴超,就必須等待已經(jīng)在寫的協(xié)程結(jié)束寫操作。

讀寫鎖是RWMutex鸯乃,它有5個函數(shù)鲸阻,它需要為讀操作和寫操作分別提供鎖操作,這樣就4個了:

  • Lock()Unlock()是給寫操作用的缨睡。
  • RLock()RUnlock()是給讀操作用的鸟悴。

RLocker()能獲取讀鎖,然后傳遞給其他協(xié)程使用奖年。使用較少细诸。

type RWMutex
func (rw *RWMutex) Lock(){}
func (rw *RWMutex) RLock(){}
func (rw *RWMutex) RLocker() Locker{}
func (rw *RWMutex) RUnlock(){}
func (rw *RWMutex) Unlock(){}

上面的銀行實現(xiàn)不合理:大家都是拿手機APP查余額,可以同時幾個人一起查呀陋守,這根本不影響震贵,銀行的鎖可以換成讀寫鎖。存水评、取錢是寫操作猩系,查詢金額是讀操作,代碼修改如下中燥,其他不變:

type Bank struct {
    sync.RWMutex
    saving map[string]int // 每賬戶的存款金額
}

// Query 查詢余額
func (b *Bank) Query(name string) int {
    b.RLock()
    defer b.RUnlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }

    return b.saving[name]
}

func main() {
    b := NewBank()
    go b.Deposit("xiaoming", 100)
    go b.Withdraw("xiaoming", 20)
    go b.Deposit("xiaogang", 2000)

    time.Sleep(time.Second)
    print := func(name string) {
        fmt.Printf("%s has: %d\n", name, b.Query(name))
    }

    nameList := []string{"xiaoming", "xiaogang", "xiaohong", "xiaozhang"}
    for _, name := range nameList {
        go print(name)
    }

    time.Sleep(time.Second)
}

結(jié)果寇甸,可能不一樣,因為協(xié)程都是并發(fā)執(zhí)行的,執(zhí)行順序不固定

?  sync_pkg git:(master) ? go run rwmutex.go
xiaohong has: 0
xiaozhang has: 0
xiaogang has: 2000
xiaoming has: 100

等待組

互斥鎖和讀寫鎖大多數(shù)人可能比較熟悉拿霉,而對等待組(WaitGroup)可能就不那么熟悉吟秩,甚至有點陌生,所以先來介紹下等待組在現(xiàn)實中的例子绽淘。

你們團隊有5個人涵防,你作為隊長要帶領(lǐng)大家打開藏有寶藏的箱子,但這個箱子需要4把鑰匙才能同時打開收恢,你把尋找4把鑰匙的任務(wù)武学,分配給4個隊員祭往,讓他們分別去尋找伦意,而你則守著寶箱,在這等待硼补,等他們都找到回來后驮肉,一起插進(jìn)鑰匙打開寶箱。

這其中有個很重要的過程叫等待:等待一些工作完成后已骇,再進(jìn)行下一步的工作离钝。如果使用Golang實現(xiàn),就得使用等待組褪储。

等待組是WaitGroup卵渴,它有3個函數(shù):

  • Add():在被等待的協(xié)程啟動前加1,代表要等待1個協(xié)程鲤竹。
  • Done():被等待的協(xié)程執(zhí)行Done浪读,代表該協(xié)程已經(jīng)完成任務(wù),通知等待協(xié)程辛藻。
  • Wait(): 等待其他協(xié)程的協(xié)程碘橘,使用Wait進(jìn)行等待。
type WaitGroup
func (wg *WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}

來吱肌,一起看下怎么用WaitGroup實現(xiàn)上面的問題痘拆。

隊長先創(chuàng)建一個WaitGroup對象wg,每個隊員都是1個協(xié)程氮墨, 隊長讓隊員出發(fā)前纺蛆,使用wg.Add(),隊員出發(fā)尋找鑰匙规揪,隊長使用wg.Wait()等待(阻塞)所有隊員完成桥氏,某個隊員完成時執(zhí)行wg.Done(),等所有隊員找到鑰匙粒褒,wg.Wait()則返回识颊,完成了等待的過程,接下來就是開箱。

結(jié)合之前的協(xié)程池的例子祥款,修改成WG等待協(xié)程池協(xié)程退出清笨,實例代碼:

func leader() {
    var wg sync.WaitGroup
    wg.Add(4)
    for i := 0; i < 4; i++ {
        go follower(&wg, i)
    }
    wg.Wait()
    
    fmt.Println("open the box together")
}

func follower(wg *sync.WaitGroup, id int) {
    fmt.Printf("follwer %d find key\n", id)
    wg.Done()
}

結(jié)果:

?  sync_pkg git:(master) ? go run waitgroup.go
follwer 3 find key
follwer 1 find key
follwer 0 find key
follwer 2 find key
open the box together

WaitGroup也常用在協(xié)程池的處理上,協(xié)程池等待所有協(xié)程退出刃跛,把上篇文章《Golang并發(fā)模型:輕松入門協(xié)程池》的例子改下:

func workerPool(n int, jobCh <-chan int, retCh chan<- string) {
    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go worker(&wg, i, jobCh, retCh)
    }

    wg.Wait()
    close(retCh)
}

func worker(wg *sync.WaitGroup, id int, jobCh <-chan int, retCh chan<- string) {
    cnt := 0
    for job := range jobCh {
        cnt++
        ret := fmt.Sprintf("worker %d processed job: %d, it's the %dth processed by me.", id, job, cnt)
        retCh <- ret
    }

    wg.Done()
}

單次執(zhí)行

在程序執(zhí)行前抠艾,通常需要做一些初始化操作,但觸發(fā)初始化操作的地方是有多處的桨昙,但是這個初始化又只能執(zhí)行1次检号,怎么辦呢?

使用Once就能輕松解決蛙酪,once對象是用來存放1個無入?yún)o返回值的函數(shù)齐苛,once可以確保這個函數(shù)只被執(zhí)行1次

type Once
func (o *Once) Do(f func()){}

直接把官方代碼給大家搬過來看下桂塞,once在10個協(xié)程中調(diào)用凹蜂,但once中的函數(shù)onceBody()只執(zhí)行了1次:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }
}

結(jié)果:

?  sync_pkg git:(master) ? go run once.go
Only once

示例源碼

本文所有示例源碼,及歷史文章阁危、代碼都存儲在Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/sync_pkg

下期預(yù)告

這次先介紹入門的知識玛痊,下次再介紹一些深入思考、最佳實踐狂打,不能一口吃個胖子擂煞,咱們慢慢來,順序漸進(jìn)趴乡。

下一篇我以這些主題進(jìn)行介紹对省,歡迎關(guān)注:

  1. 哪個協(xié)程先獲取鎖
  2. 一定要用鎖嗎
  3. 鎖與通道的選擇

文章推薦

  1. Golang并發(fā)模型:輕松入門流水線模型
  2. Golang并發(fā)模型:輕松入門流水線FAN模式
  3. Golang并發(fā)模型:并發(fā)協(xié)程的優(yōu)雅退出
  4. Golang并發(fā)模型:輕松入門select
  5. Golang并發(fā)模型:select進(jìn)階
  6. Golang并發(fā)模型:輕松入門協(xié)程池
  7. Golang并發(fā)的次優(yōu)選擇:sync包
  1. 如果這篇文章對你有幫助,請點個贊/喜歡浙宜,感謝官辽。
  2. 本文作者:大彬
  3. 如果喜歡本文,隨意轉(zhuǎn)載粟瞬,但請保留此原文鏈接:http://lessisbetter.site/2019/01/04/golang-pkg-sync/
一起學(xué)Golang-分享有料的Go語言技術(shù)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末同仆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子裙品,更是在濱河造成了極大的恐慌俗批,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件市怎,死亡現(xiàn)場離奇詭異岁忘,居然都是意外死亡,警方通過查閱死者的電腦和手機区匠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門干像,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帅腌,“玉大人,你說我怎么就攤上這事麻汰∷倏停” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵五鲫,是天一觀的道長溺职。 經(jīng)常有香客問我,道長位喂,這世上最難降的妖魔是什么浪耘? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮塑崖,結(jié)果婚禮上七冲,老公的妹妹穿的比我還像新娘。我一直安慰自己弃舒,他們只是感情好癞埠,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布状原。 她就那樣靜靜地躺著聋呢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪颠区。 梳的紋絲不亂的頭發(fā)上削锰,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天,我揣著相機與錄音毕莱,去河邊找鬼器贩。 笑死,一個胖子當(dāng)著我的面吹牛朋截,可吹牛的內(nèi)容都是我干的蛹稍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼部服,長吁一口氣:“原來是場噩夢啊……” “哼唆姐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起廓八,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤奉芦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后剧蹂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體声功,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年宠叼,在試婚紗的時候發(fā)現(xiàn)自己被綠了先巴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖伸蚯,靈堂內(nèi)的尸體忽然破棺而出醋闭,到底是詐尸還是另有隱情,我是刑警寧澤朝卒,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布证逻,位于F島的核電站,受9級特大地震影響抗斤,放射性物質(zhì)發(fā)生泄漏囚企。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一瑞眼、第九天 我趴在偏房一處隱蔽的房頂上張望龙宏。 院中可真熱鬧,春花似錦伤疙、人聲如沸银酗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽黍特。三九已至,卻和暖如春锯蛀,著一層夾襖步出監(jiān)牢的瞬間灭衷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工旁涤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留翔曲,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓劈愚,卻偏偏與公主長得像瞳遍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子菌羽,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

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