Golang併發程式設計有緩衝通道和無緩衝通道(channel)

OldBoy~發表於2018-09-16

無緩衝通道

是指在接收前沒有能力儲存任何值得通道。
這種型別的通道要求傳送goroutine和接收goroutine同時準備好,才能完成傳送和接收操作。如果兩個goroutine沒有同時準備好,通道會導致先執行傳送或接收操作的goroutine阻塞等待。
這種對通道進行傳送和接收的互動行為本身就是同步的,其中任意一個操作都無法離開另一個操作單獨存在。

上圖所示,如同接力賽。根據圖編號觀察①兩個協程,建立好了通道②一個往通道里放,這時候兩邊阻塞④這時候另一個協程要接⑤另一個協程取出來,從①-⑤都是阻塞的,⑥才完成交接,才不會阻塞。

再比喻: 就是一個送信人去你家門口送信 ,你不在家 他不走,你一定要接下信,他才會走。

無緩衝channel建立

ch := make(chan int, 0)  //第二個引數為0,或者不寫第二個引數

如果沒有指定緩衝區容量,那麼該通道就是同步的,因此會阻塞到傳送者準備好傳送和接收者準備好接收。

程式碼案例

package main

import (
    "fmt"
    "time"
)

func main() {
    //建立一個無快取的channel
    ch := make(chan int, 0)

    //len(ch)緩衝區剩餘資料個數, cap(ch)緩衝區大小,兩者這裡永遠都是0
    fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))

    //新建協程
    go func() {
        for i := 0; i < 3; i++ { //寫三次
            fmt.Printf("子協程:i = %d\n", i)
            ch <- i //往chan寫內容
            fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))
        }
    }()

    //延時2秒
    time.Sleep(2 * time.Second)

    for i := 0; i < 3; i++ { //必須讀三次
        num := <-ch //讀管道中內容,沒有內容前,阻塞
        fmt.Println("num = ", num)
    }

}
len(ch) = 0, cap(ch)= 0
子協程:i = 0
num =  0
len(ch) = 0, cap(ch)= 0
子協程:i = 1
len(ch) = 0, cap(ch)= 0
子協程:i = 2
num =  1
num =  2

流程分析

主協程執行到這裡,阻塞兩秒
//延時2秒
time.Sleep(2 * time.Second)

//兩秒鐘時間子協程肯定把for迴圈執行完畢,但這裡也會出現阻塞
//阻塞原因是:當執行到往通道寫資料是無緩衝的,對方不讀之前會阻塞。也就是,在主協程等著子協程寫完,但是主協程還沒到讀的時候,這時候出現阻塞,等到主協程讀完資料才會往下走。
可以執行觀察一下程式的執行卡頓觀察阻塞
for i := 0; i < 3; i++ { //寫三次 fmt.Printf("子協程:i = %d\n", i) ch <- i //往chan寫內容 fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch)) }

列印結果分析:首先子協程for迴圈往管道里寫入一個資料,緊接著主協程for迴圈出現阻塞,然後主協程for迴圈從管道讀資料,讀完了列印。主協程列印完,子協程for迴圈繼續給管道資料,但也有可能主協程讀完資料沒來得及列印,子協程就把資料寫入管道並列印完畢,因為兩個是同時並行的。

有緩衝通道

 指通道可以儲存多個值。

如果給定了一個緩衝區容量,那麼通道就是非同步的,只要緩衝區有未使用空間用於傳送資料,或還包含可以接收的資料,那麼其通訊就會無阻塞地進行

上圖所示:

①右側的goroutine正在從通道接收一個值。
②右側的goroutine獨立完成了接手值得動作,而左側的goroutine正在傳送一個新值到通道里。
③左側的goroutine還在向通道傳送新值,而右側的goroutine正在從通道接收另一個值。這個步驟裡的兩個操作既不是同步,也不會互相阻塞。
④所有的傳送和接收都完成,而通道里還有幾個值,也有一些空間可以儲存更多的值

有緩衝channel建立

ch := make(chan int, 3)  //容量是3

程式碼案例

package main

import (
    "fmt"
    "time"
)

func main() {
    //建立一個有快取的channel
    ch := make(chan int, 3) //容量是3

    //len(ch)緩衝區剩餘資料個數, cap(ch)緩衝區大小
    fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))

    //新建協程
    go func() {
        for i := 0; i < 10; i++ { //這裡資料量大於管道容量,會出阻塞
            ch <- i //往chan寫內容,如果主協程沒讀的話,寫滿3個就會阻塞在此
            fmt.Printf("子協程[%d]: len(ch) = %d, cap(ch)= %d\n", i, len(ch), cap(ch))
        }
    }()

    //延時
    time.Sleep(2 * time.Second)

    for i := 0; i < 10; i++ { //這裡資料量大於管道容量,會出阻塞
        num := <-ch //讀管道中內容,沒有內容前,阻塞
        fmt.Println("num = ", num)
    }

}
len(ch) = 0, cap(ch)= 3
子協程[0]: len(ch) = 1, cap(ch)= 3
子協程[1]: len(ch) = 2, cap(ch)= 3
子協程[2]: len(ch) = 3, cap(ch)= 3
num =  0
num =  1
num =  2
num =  3
子協程[3]: len(ch) = 3, cap(ch)= 3
子協程[4]: len(ch) = 0, cap(ch)= 3
子協程[5]: len(ch) = 1, cap(ch)= 3
子協程[6]: len(ch) = 2, cap(ch)= 3
子協程[7]: len(ch) = 3, cap(ch)= 3
num =  4
num =  5
num =  6
num =  7
num =  8
子協程[8]: len(ch) = 3, cap(ch)= 3
子協程[9]: len(ch) = 0, cap(ch)= 3
num =  9

總結一下有緩衝channel和無緩衝channel的特點與不同

無緩衝的與有緩衝channel有著重大差別,那就是一個是同步的 一個是非同步的。

比如
c1:=make(chan int) 無緩衝
c2:=make(chan int,1) 有緩衝
c1<-1 

無緩衝: 不僅僅是向 c1 通道放 1,而是一直要等有別的攜程 <-c1 接手了這個引數,那麼c1<-1才會繼續下去,要不然就一直阻塞著。
有緩衝: c2<-1 則不會阻塞,因為緩衝大小是1(其實是緩衝大小為0),只有當放第二個值的時候,第一個還沒被人拿走,這時候才會阻塞。

 

相關文章