iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 17
0

師生戀母湯母湯

小櫻乖孩子不要亂看


Buffer 的中文稱為緩充,這是一個相當不好理解的概念。所以我舉個例子:

有 buffer 的 channel 就如同:

今天小櫻和同學一行人想要走到知世家,假設小櫻到知世家的路上最多只能有 3 個人,只要多一個人Golang牌就會原地爆炸,小可會變成肉醬。但是小櫻和同學們又想趕快到知世家,因此只要有這 3 個人中其中一個人抵達知世家後就會通知小櫻家可以再走下一個人了。

沒有 buffer 的 channel 就如同:

今天小櫻和同學一行人想要走到知世家,假設小櫻到知世家的路上最多只能有 1 個人,只要多一個人Golang牌就會原地爆炸,小可會變成肉醬。但是小櫻和同學們又想趕快到知世家,因此只要唯一的那個人抵達知世家後就會通知小櫻家可以再走下一個人了。

進入課程之前 ── 使用匿名函式

要開始今天課程前,先介紹匿名函式,匿名函式的意思就是沒有名字的函式。函式有 scope 的特性,函式內的參數只有函式內能用,而函式外的變數則能輕易被函式所讀取。因此有時需要 scope 的特性又不想在另外宣告,這時就會使用匿名函式。除此之外,當我需要使用 go 關鍵字新開一條執行緒時通常要「搭配呼叫函式」這個動作,但每次都要另外宣告,太麻煩了,因此可以改用匿名函式的方式使用。

1. 可以將函式視為變數

package main
import "fmt"

func main(){
    add := func(a, b int){
        fmt.Println(a + b)
    }
    add(3, 4)
}

執行結果:
7

2. 宣告後直接使用,更方便

package main
import "fmt"

func main(){
    func(a, b int){
        fmt.Println(a + b)
    }(3, 4)
}

執行結果:
7

我們可以想像 func(a, b int){...} 這整串叫 add 而後面接上 (3, 4) 就是 add(3, 4)

3. 如同一般的函式,匿名函式亦可以取得函式外的變數

package main
import "fmt"

func main(){
    str := "化作懲戒的鎖鏈吧!" // 這是一個函式外的變數
    func(){
        fmt.Println(str)
    }()
}

執行結果:
化作懲戒的鎖鏈吧!

無緩充通道 Unbuffered Channel

這是一個沒有緩充的 channel

package main
import "fmt"

func main(){
    no_buffer_chan := make(chan int)
    
    // 搭配匿名函式使用
    go func(){
        no_buffer_chan <- 1
        no_buffer_chan <- 2
        no_buffer_chan <- 3
    }()

    fmt.Println(<- no_buffer_chan)
    fmt.Println(<- no_buffer_chan)
    fmt.Println(<- no_buffer_chan)
}

執行結果:
1
2
3

在每次要傳送或接收前都必需進行等待,這就是沒有 buffer 的 channel 的特性

package main
import "fmt"

func main(){
    no_buffer_chan := make(chan int)
    go func(){
        fmt.Println("傳送1")
        no_buffer_chan <- 1
        fmt.Println("傳送2")
        no_buffer_chan <- 2
        fmt.Println("傳送3")
        no_buffer_chan <- 3
    }()
    fmt.Println("等待接收1")
    fmt.Println(<- no_buffer_chan)
    fmt.Println("等待接收2")
    fmt.Println(<- no_buffer_chan)
    fmt.Println("等待接收3")
    fmt.Println(<- no_buffer_chan)
}

執行結果:
等待接收1
傳送1
傳送2
等待接收2
等待接收3
傳送3

(每次執行結果會不太一樣)

各位魔法使們可以多嘗試幾次,將可以發現等待和傳送之間微妙的關係, 不會出現以下這種結果:

傳送1
傳送2
傳送3
等待接收1
等待接收2
等待接收3

因為對於一個沒有 buffer 的 channel ,channel 上一次只能有一個值,前一個傳完,才能傳下一個,不能允許一次上傳超過一個

有緩充通道 Buffered Channel

沒有 buffer 的 channel 是一個傳送到達才能再送下一個,如果要避免這種浪費時間的情況,我們可以選擇使用帶有 buffer 的 channel 傳送

沒有 buffer 的 channel

一次有 3 個 buffer 的 channel

如果是有 buffer 的 channel 那麼就 不必在意對方是否接收到 ,只要在以是否仍有足夠的緩充空間,如果沒有空間了才會進行等待。

這次改以有 buffer 的 channel 實作。宣告有 buffer 的 channel 時,只要在第二個參數加上 buffer 的大小即可

package main
import "fmt"

func main(){
    buffer_chan := make(chan int, 3)
    go func(){
        fmt.Println("傳送1")
        buffer_chan <- 1
        fmt.Println("傳送2")
        buffer_chan <- 2
        fmt.Println("傳送3")
        buffer_chan <- 3
    }()
    fmt.Println("等待接收1")
    <- buffer_chan
    fmt.Println("等待接收2")
    <- buffer_chan
    fmt.Println("等待接收3")
    <- buffer_chan
}

執行結果
傳送1
傳送2
傳送3
等待接收1
等待接收2
等待接收3

3 個 buffer 的 channel,要傳送 5 個值

如果 buffer 滿了又想要傳送到 channel,那麼傳送端就會進行等待,如同無緩充的 buffer 一樣。但是如果遲遲沒有接收走,就會出錯

package main
import "fmt"

func main(){
    buffer_chan := make(chan int, 3)
    go func(){
        fmt.Println("等待接收1")
        <- buffer_chan
    }()
    fmt.Println("傳送1")
    buffer_chan <- 1
    fmt.Println("傳送2")
    buffer_chan <- 2
    fmt.Println("傳送3")
    buffer_chan <- 3
    fmt.Println("傳送4")
    buffer_chan <- 4

    // 出錯,因為 buffer 滿了,主執行緒會一直等待
    fmt.Println("傳送5")
    buffer_chan <- 4
}

執行結果:
傳送1
傳送2
傳送3
傳送4
等待接收1
傳送5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
C:/Users/liao2/OneDrive/go-tutorial/lesson17b.go:21 +0x2bf
exit status 2

從以上程式很明顯的可以看到一開始傳送了 1, 2, 3 到 channel 因為子執行緒接收了 1 ,所以 channel 上是 2, 3, 空,因為有空位,所以 4 也成功傳上變成 2, 3, 4 ,但是,因為再也沒有人接收 channel 了,所以 5 遲遲無法傳送,導致 deadlock

接收不確定個數的 channel

因為有時候無法確定究竟有幾個值會傳送到 channel 上,所以接收端很難判定(可以用無緩充channel實作,但很不直觀)。因此可以利用 for + range 來接收 channel 上的值,但是要注意一點,傳送最後一個值後必需利用 close() 將 channel 關閉,golang 才知道這是最後一個值

package main
import "fmt"

func main(){
    buffer_chan := make(chan int, 3)
    go func(){
        for i := 0; i < 10; i = i+1{
            buffer_chan <- i
        }
        close(buffer_chan)
    }()
    for k := range buffer_chan {
        fmt.Println(k)
    }
}

執行結果:
0
1
2
3
4
5
6
7
8
9

效能分析(有緩充及無緩充)

因為無緩充的 channel 一次只能傳送一個值,所以會花很多時間做等待。但是如果不測給你們看,就無憑無據

package main
import (
    "fmt"
    "time"
)

func main(){
    // 無緩充
    start := time.Now().UnixNano()
    channel := make(chan int)
    go func(){
        for i := 0; i < 10000; i = i+1{
            channel <- i
        }
        close(channel)
    }()
    for _ = range channel {
    }
    fmt.Println((time.Now().UnixNano() - start), "ns")

    ////////////////////////////////////

    // 有緩充
    start = time.Now().UnixNano()
    buffer_channel := make(chan int, 100)
    go func(){
        for i := 0; i < 10000; i = i+1{
            buffer_channel <- i
        }
        close(buffer_channel)
    }()
    for _ = range buffer_channel {
    }
    fmt.Println((time.Now().UnixNano() - start), "ns")
}

執行結果:
3987500 ns
1093700 ns

每次執行都不太一樣,但很明顯有緩充的channel效能比沒有緩充來的好

後記

國二生跟女老師交往,還交往一年,小櫻他哥真的猛 respect!

本文大多數圖片來自:
庫洛魔法使第一季第廿七集


上一篇
#16 Go routine ── 前無古人後無來者,地表最輕量、最快速的執行緒 | Golang魔法使
下一篇
#18 同步、異步與互斥鎖 ── sync 套件實戰 | Golang魔法使
系列文
Golang魔法使 ─ 30天從零開始學習Go語言 | 比Python還簡單 | 理科生一定學得會 | 文科生不好說30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言