go 并發(fā)之 channel

CSP 并發(fā)模型

CSP(Communicating Sequential Processes),是用于描述兩個(gè)獨(dú)立的并發(fā)實(shí)體通過共享 channel(管道)進(jìn)行通信的并發(fā)模型。

go 并發(fā)模型

Go 語言中有兩種并發(fā)編程模型,除了普遍認(rèn)知的多線程共享內(nèi)存模型,還把 CSP 的思想融入到語言的核心里,基于 goroutine 和 channel 實(shí)現(xiàn)了其特有的 CSP 并發(fā)模型,使并發(fā)編程成為 Go 的一個(gè)獨(dú)特優(yōu)勢(shì)。

goroutine 和 channel 是一對(duì)組合。goroutine 是執(zhí)行并發(fā)的實(shí)體,而每個(gè)實(shí)體之間通過 channel 通信來實(shí)現(xiàn)數(shù)據(jù)共享。

go 并發(fā)原則

Do not communicate by sharing memory; instead, share memory by communicating.

不要通過共享內(nèi)存來通信,而要通過通信來實(shí)現(xiàn)內(nèi)存共享。

即不推薦使用 sync 包里的 mutex 等組件,而是使用 channel 進(jìn)行并發(fā)編程。可以使用原子函數(shù)、互斥鎖等 ,但使用 channel 更優(yōu)雅。

但兩者其實(shí)都是必要且有效的。實(shí)際上 channel 的底層就是通過 mutex 來控制并發(fā)的,只是 channel 是更高一層次的并發(fā)編程原語,封裝了更多的功能。

是選擇 sync 包里的底層并發(fā)編程原語還是 channel,參考如下決策樹:

圖片

channel

什么是 channel

channel 是 goroutine 之間通信的管道。channel 是線程安全的,并提供“先進(jìn)先出”的特性。

使用

聲明和初始化

var c chan int        // 聲明了一個(gè) nil 通道
c = make(chan int)    // 初始化了一個(gè)無緩沖通道,其值是一個(gè)地址,類型是 chan int
c = make(chan int, 100) // 初始化了一個(gè)緩沖區(qū)大小為100的通道

發(fā)送和接收

go func() {c <- 100}()  // 發(fā)送數(shù)據(jù)到通道中
i := <-c                // 從通道中接收數(shù)據(jù)

實(shí)現(xiàn)原理

數(shù)據(jù)結(jié)構(gòu)

type hchan struct {
    // chan 里元素?cái)?shù)量
    qcount   uint
    // chan 底層循環(huán)數(shù)組的長(zhǎng)度
    dataqsiz uint
    // 指向底層循環(huán)數(shù)組的指針,只針對(duì)有緩沖的 channel
    buf      unsafe.Pointer
    // chan 中元素大小
    elemsize uint16
    // chan 是否被關(guān)閉的標(biāo)志
    closed   uint32
    // chan 中元素類型
    elemtype *_type // element type
    // 已發(fā)送元素在循環(huán)數(shù)組中的索引
    sendx    uint   // send index
    // 已接收元素在循環(huán)數(shù)組中的索引
    recvx    uint   // receive index
    // 等待接收的 goroutine 隊(duì)列
    recvq    waitq  // list of recv waiters
    // 等待發(fā)送的 goroutine 隊(duì)列
    sendq    waitq  // list of send waiters

    // 保護(hù) hchan 中所有字段
    lock mutex
}

buf 指向底層循環(huán)數(shù)組,只有緩沖型的 channel 才有。

sendxrecvx 均指向底層循環(huán)數(shù)組,表示當(dāng)前可以發(fā)送和接收的元素位置索引值(相對(duì)于底層數(shù)組)。

sendqrecvq 分別表示被阻塞的 goroutine,這些 goroutine 由于嘗試讀取 channel 或向 channel 發(fā)送數(shù)據(jù)而被阻塞。

waitqsudog 的一個(gè)雙向鏈表,而 sudog 實(shí)際上是對(duì) goroutine 的一個(gè)封裝:

type waitq struct {    
    first *sudog    
    last  *sudog
}

lock 用來保證每個(gè)讀 channel 或?qū)?channel 的操作都是原子的。

例如,創(chuàng)建一個(gè)容量為 6 的,元素為 int 型的 channel 數(shù)據(jù)結(jié)構(gòu)如下 :

圖片

創(chuàng)建

當(dāng)使用 make 函數(shù)創(chuàng)建通道時(shí),底層創(chuàng)建函數(shù)如下:

func makechan(t *chantype, size int64) *hchan

從函數(shù)原型來看,創(chuàng)建的 chan 是一個(gè)指針,和 map 相似。

接收和發(fā)送

對(duì) channel 的發(fā)送和接收操作都會(huì)在編譯期間轉(zhuǎn)換成為底層的發(fā)送接收函數(shù)。

channel 的發(fā)送和接收操作本質(zhì)上都是 “值的拷貝”。

緩沖通道

對(duì)于無緩沖通道,讀寫通道會(huì)立馬阻塞當(dāng)前協(xié)程。一個(gè)協(xié)程被通道操作阻塞后,Go 調(diào)度器會(huì)去調(diào)用其他可用的協(xié)程,這樣程序就不會(huì)一直阻塞。

而對(duì)于緩沖通道,寫不會(huì)阻塞當(dāng)前通道,直到通道滿了,同理,讀操作也不會(huì)阻塞當(dāng)前通道,除非通道沒數(shù)據(jù)。

也可以說無緩沖的 channel 是同步的,而有緩沖的 channel 是異步的。

創(chuàng)建一個(gè)緩沖通道:

ch := make(chan type, capacity)

capacity 是緩沖大小,必須大于 0。內(nèi)置函數(shù) len()、cap() 可以計(jì)算通道的長(zhǎng)度和容量。

如果緩沖通道是關(guān)閉狀態(tài)但有數(shù)據(jù),仍然可以讀取數(shù)據(jù)。

單向通道

主要用在通道作為參數(shù)傳遞的時(shí)候,Go 提供了自動(dòng)轉(zhuǎn)化,雙向轉(zhuǎn)單向。

使用單向通道主要是可以提高程序的類型安全性,程序不容易出錯(cuò)。

關(guān)閉通道

使用內(nèi)置函數(shù)close(ch)可以關(guān)閉通道。

判斷通道關(guān)閉狀態(tài)

  • 數(shù)據(jù)接收方可以通過返回狀態(tài)判斷通道是否已關(guān)閉:

    val, ok := <- ch
    

    ok 用于判斷 ch 是否已關(guān)閉且沒有緩沖值可以讀取。為 true,該通道還可以進(jìn)行讀寫操作;為 false,通道已關(guān)閉且沒有緩沖數(shù)據(jù),不能再進(jìn)行數(shù)據(jù)傳輸,返回的對(duì)應(yīng)類型的零值。

  • for range 讀取通道,通道關(guān)閉,for range 自動(dòng)退出

    for v := range ch {
      fmt.Println(v)
    }
    

    應(yīng)注意,使用 for range 讀取一個(gè)通道,數(shù)據(jù)寫入完畢后必須關(guān)閉通道,否則協(xié)程阻塞。

如何優(yōu)雅地關(guān)閉 channel

關(guān)于 channel 的使用,有幾點(diǎn)不方便的地方:

  1. 在不改變 channel 自身狀態(tài)的情況下,無法獲知一個(gè) channel 是否關(guān)閉。
  2. 關(guān)閉一個(gè) closed channel 會(huì)導(dǎo)致 panic。所以,如果關(guān)閉 channel 的一方在不知道 channel 是否處于關(guān)閉狀態(tài)時(shí)就去貿(mào)然關(guān)閉 channel 是很危險(xiǎn)的事情。
  3. 向一個(gè) closed channel 發(fā)送數(shù)據(jù)會(huì)導(dǎo)致 panic。所以,如果向 channel 發(fā)送數(shù)據(jù)的一方不知道 channel 是否處于關(guān)閉狀態(tài)時(shí)就去貿(mào)然向 channel 發(fā)送數(shù)據(jù)是很危險(xiǎn)的事情。

應(yīng)該如何優(yōu)雅地關(guān)閉 channel?

根據(jù) sender 和 receiver 的個(gè)數(shù),分下面幾種情況:

  1. 一個(gè) sender,一個(gè) receiver
  2. 一個(gè) sender, M 個(gè) receiver
  3. N 個(gè) sender,一個(gè) reciver
  4. N 個(gè) sender, M 個(gè) receiver

對(duì)于 1,2,只有一個(gè) sender 的情況就不用說了,直接從 sender 端關(guān)閉就好了。

第 3 種情形下,優(yōu)雅關(guān)閉 channel 的方法是增加一個(gè)傳遞關(guān)閉信號(hào)的 channel,receiver 通過信號(hào) channel 下達(dá)關(guān)閉數(shù)據(jù) channel 的命令。senders 監(jiān)聽到關(guān)閉信號(hào)后,停止發(fā)送數(shù)據(jù)。

func main() {
    rand.Seed(time.Now().UnixNano())

    const Max = 100000
    const NumSenders = 1000

    dataCh := make(chan int, 100)
    stopCh := make(chan struct{})

    var wg sync.WaitGroup
    wg.Add(NumSenders)

    for i := 0; i < NumSenders; i++ {
        go func() {
            for {
                select {
                case <-stopCh:
                    wg.Done()
                    return
                case dataCh <- rand.Intn(Max):
                }
            }
        }()
    }

    go func() {
        for value := range dataCh {
            if value == Max-1 {
                fmt.Println("send stop signal to senders.")
                close(stopCh)
                return
            }
            fmt.Println(value)
        }
    }()

    wg.Wait()
    fmt.Println("main exit")
}

stopCh 就是信號(hào) channel,receiver 關(guān)閉 stopCh 來通知 senders 停止發(fā)送數(shù)據(jù)。上面的代碼沒有明確關(guān)閉 dataCh,在 Go 語言中,對(duì)于一個(gè) channel,如果最終沒有任何 goroutine 引用它,不管 channel 有沒有被關(guān)閉,最終都會(huì)被 gc 回收。所以,在這種情形下,所謂的優(yōu)雅地關(guān)閉 channel 就是不關(guān)閉 channel,讓 gc 代勞。

第四種情況和第三種情況不同,這里有 M 個(gè) receiver,如果還是采用第三種方案,由 receiver 直接關(guān)閉 stopCh 的話,就會(huì)重復(fù)關(guān)閉一個(gè) channel,導(dǎo)致 panic。因此需要增加一個(gè)中間人,M 個(gè) receiver 都向他發(fā)送關(guān)閉 dataCh 的請(qǐng)求,中間人收到第一個(gè)請(qǐng)求后下達(dá)關(guān)閉 dataCh 的指令(關(guān)閉 stopCh)。當(dāng)然,這里的 N 個(gè) sender 也可以向中間人發(fā)送關(guān)閉 dataCh 的請(qǐng)求。

func main() {
    rand.Seed(time.Now().UnixNano())

    const Max = 100000
    const NumSenders = 1000
    const NumReceivers = 10

    dataCh := make(chan int, 100)
    stopCh := make(chan struct{})
    toStop := make(chan string, NumSenders + NumReceivers)
    exitCh := make(chan struct{})

    go func() {
        s := <-toStop
        fmt.Println("toStop s:", s)
        close(stopCh)
        exitCh <- struct{}{}
    }()

    for i := 0; i < NumSenders; i++ {
        go func(id string) {
            for {
                value := rand.Intn(Max)
                if value == 0 {
                    toStop <- "sender#" + id
                    return
                }
                select {
                case <-stopCh:
                    return
                case dataCh <- value:
                }
            }
        }(strconv.Itoa(i))
    }

    for i := 0; i < NumReceivers; i++ {
        go func(id string) {
            for {
                select {
                case <- stopCh:
                    return
                case value := <-dataCh:
                    if value == Max-1 {
                        toStop <- "receiver#" + id
                        return
                    }
                    fmt.Println(value)
                }
            }
        }(strconv.Itoa(i))
    }

    select {
    case <-exitCh:
        time.Sleep(time.Second)
    }
    fmt.Println("main exit")
}

代碼里 toStop 就是中間人的角色,使用它來接收 senders 和 receivers 發(fā)送過來的關(guān)閉 dataCh 請(qǐng)求。這里同樣沒有真正關(guān)閉 dataCh,讓 gc 代勞。

常見異常操作

操作 nil 或 closed channel

  • close 關(guān)閉一個(gè) nil channel,會(huì)引起 panic。

  • close 關(guān)閉一個(gè) closed channel,會(huì)引起 panic。

  • 往一個(gè) nil channel 發(fā)送數(shù)據(jù),會(huì)造成阻塞。

  • 從一個(gè) nil channel 接收數(shù)據(jù),會(huì)造成阻塞。

  • 往一個(gè) closed channel 發(fā)送數(shù)據(jù),會(huì)引起 panic。

  • 從一個(gè) closed channel 接收數(shù)據(jù),返回已緩沖數(shù)據(jù)或者零值。

阻塞、死鎖

  • 無緩沖通道寫或者讀數(shù)據(jù),當(dāng)前協(xié)程阻塞。
  • 有緩沖通道已滿,再往通道中寫數(shù)據(jù),當(dāng)前協(xié)程阻塞。
  • 使用 for range 讀取一個(gè)通道,數(shù)據(jù)寫入端寫入完畢后必須關(guān)閉通道,否則 for range 語句所在協(xié)程阻塞。
  • 空 select 語句select {},沒有 case 分支,當(dāng)前協(xié)程阻塞。
  • 當(dāng)前協(xié)程阻塞又沒有其他可用協(xié)程時(shí),死鎖。

內(nèi)存泄漏

  • channel 可能會(huì)引發(fā) goroutine 泄漏,原因是 goroutine 操作 channel 后,處于發(fā)送或接收阻塞狀態(tài),而 channel 處于滿或空的狀態(tài),一直得不到改變。同時(shí),垃圾回收器也不會(huì)回收此類資源,進(jìn)而導(dǎo)致 goroutine 一直處于等待隊(duì)列中。

通道應(yīng)用

停止信號(hào)

channel 多用于停止信號(hào)的場(chǎng)景,關(guān)閉 channel 或者向 channel 發(fā)送一個(gè)元素,使得接收 channel 的那一方獲知此信息,進(jìn)而做后續(xù)操作。

任務(wù)定時(shí)

與 timer 結(jié)合,實(shí)現(xiàn)超時(shí)控制。

// 等待 1 分鐘后,如果 dataCh 還沒有讀出數(shù)據(jù)或者被關(guān)閉,就直接結(jié)束。
select {
    case <-time.After(time.Minute):
    case <-dataCh:
        fmt.Println("do something")
}

或定期執(zhí)行任務(wù)。

// 每隔一秒執(zhí)行任務(wù)
ticker := time.Tick(time.Second)
for {
    select {
        case <- ticker:
        fmt.Println("do something")
    }
}

解耦生產(chǎn)方和消費(fèi)方

生產(chǎn)方往 taskCh 塞任務(wù),消費(fèi)方循環(huán)從 channel 中拿任務(wù),解耦。

控制并發(fā)數(shù)

某些場(chǎng)景因?yàn)橘Y源限制等原因,需要控制并發(fā)數(shù)量。

limit := make(chan int, 3)
for _, w := range work {
    go func() {
        limit <- 1
        w()
        <-limit
    }()
}

真正執(zhí)行任務(wù),訪問第三方的動(dòng)作在 w() 中完成,在執(zhí)行 w() 之前,先要從 limit 中拿“許可證”,拿到許可證之后,才能執(zhí)行 w(),并且在執(zhí)行完任務(wù),要將“許可證”歸還。這樣就可以控制同時(shí)運(yùn)行的 goroutine 數(shù)。

還有一點(diǎn)要注意的是,如果 w() 發(fā)生 panic,那“許可證”可能就還不回去了,因此需要使用 defer 來保證。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,616評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,355評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,727評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,928評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,165評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評(píng)論 1 282
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,585評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,892評(píng)論 2 372

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

  • 并發(fā)的概念及其重要性 這段是簡(jiǎn)單科普,大佬可以跳過 并發(fā):并發(fā)程序指同時(shí)進(jìn)行多個(gè)任務(wù)的程序。在操作系統(tǒng)中,是指一個(gè)...
    亦一銀河閱讀 760評(píng)論 0 1
  • 系統(tǒng)文件介紹 在程序啟動(dòng)運(yùn)行時(shí),自動(dòng)打開,運(yùn)行結(jié)束,自動(dòng)關(guān)閉。 鍵盤(硬件)—— 標(biāo)準(zhǔn)輸入(文件)stdin —...
    泡泡龍吐泡泡閱讀 5,011評(píng)論 0 2
  • 單純地將函數(shù)并發(fā)執(zhí)行是沒有意義的,函數(shù)與函數(shù)之間需要交換數(shù)據(jù)才能體現(xiàn)并發(fā)執(zhí)行函數(shù)的作用。雖然可使用共享內(nèi)存進(jìn)行數(shù)據(jù)...
    JunChow520閱讀 430評(píng)論 0 2
  • 目錄 一、 Go的并發(fā)機(jī)制:線程模型[http://www.lxweimin.com/p/8063a82edf0e...
    Jan_gogogo閱讀 437評(píng)論 0 0
  • 8.1 并發(fā)的含義 并發(fā):邏輯上具備同時(shí)處理多個(gè)任務(wù)的能力 并行: 物理上在同一時(shí)刻執(zhí)行多個(gè)并發(fā)任務(wù) 多線程或多進(jìn)...
    leon4ever閱讀 249評(píng)論 0 0