Golang的select/非緩衝的Channel實例詳解

select

golang 的 select 就是監聽 IO 操作,當 IO 操作發生時,觸發相應的動作。
在執行select語句的時候,運行時系統會自上而下地判斷每個case中的發送或接收操作是否可以被立即執行【立即執行:意思是當前Goroutine不會因此操作而被阻塞,還需要依據通道的具體特性(緩存或非緩存)】

  • 每個case語句裏必須是一個IO操作
  • 所有channel表達式都會被求值、所有被髮送的表達式都會被求值
  • 如果任意某個case可以進行,它就執行(其他被忽略)
  • 如果有多個case都可以運行,Select會隨機公平地選出一個執行(其他不會執行)
  • 如果有default子句,case不滿足條件時執行該語句。
  • 如果沒有default字句,select將阻塞,直到某個case可以運行;Go不會重新對channel或值進行求值。

select 語句用法

注意到 select 的代碼形式和 switch 非常相似, 不過 select 的 case 裏的操作語句只能是【IO 操作】 。
此示例裏面 select 會一直等待等到某個 case 語句完成, 也就是等到成功從 ch1 或者 ch2 中讀到數據,如果都不滿足條件且存在default case, 那麼default case會被執行。 則 select 語句結束。
示例:

package main

import (
    "fmt"
)

func main(){
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)

    select {
        case e1 := <-ch1:
        //如果ch1通道成功讀取數據,則執行該case處理語句
            fmt.Printf("1th case is selected. e1=%v",e1)
        case e2 := <-ch2:
        //如果ch2通道成功讀取數據,則執行該case處理語句
            fmt.Printf("2th case is selected. e2=%v",e2)
        default:
        //如果上面case都沒有成功,則進入default處理流程
            fmt.Println("default!.")
    }
}

select分支選擇規則

所有跟在case關鍵字右邊的發送語句或接收語句中的通道表達式和元素表達式都會先被求值。無論它們所在的case是否有可能被選擇都會這樣。

求值順序:自上而下、從左到右
示例:

package main

import (
    "fmt"
)
//定義幾個變量,其中chs和numbers分別代表了包含了有限元素的通道列表和整數列表
var ch1 chan int
var ch2 chan int
var chs = []chan int{ch1, ch2}
var numbers = []int{1,2,3,4,5}

func main(){
    select {
        case getChan(0) <- getNumber(2):
            fmt.Println("1th case is selected.")
        case getChan(1) <- getNumber(3):
            fmt.Println("2th case is selected.")
        default:
            fmt.Println("default!.")
    }
}

func getNumber(i int) int {
    fmt.Printf("numbers[%d]\n", i)
    return numbers[i]
}

func getChan(i int) chan int {
    fmt.Printf("chs[%d]\n", i)
    return chs[i]
}

輸出:

chs[0]
numbers[2]
chs[1]
numbers[3]
default!.

可以看出求值順序。滿足自上而下、自左而右這條規則。

隨機執行case

如果同時有多個case滿足條件,通過一個僞隨機的算法決定哪一個case將會被執行。
示例:

package main

import (
    "fmt"
)
func main(){
    chanCap := 5
    ch7 := make(chan int, chanCap)

    for i := 0; i < chanCap; i++ {
        select {
            case ch7 <- 1:
            case ch7 <- 2:
            case ch7 <- 3:
        }
    }

    for i := 0; i < chanCap; i++ {
        fmt.Printf("%v\n", <-ch7)
    }
}

輸出:(注:每次運行都會不一樣)

3
3
2
3
1

一些慣用手法示例

示例一:單獨啓用一個Goroutine執行select,等待通道關閉後結束循環

package main

import (
    "fmt"
    "time"
)
func main(){
    //初始化通道
    ch11 := make(chan int, 1000)
    sign := make(chan int, 1)

    //給ch11通道寫入數據
    for i := 0; i < 1000; i++ {
        ch11 <- i
    }
    //關閉ch11通道
    close(ch11)

    //單獨起一個Goroutine執行select
    go func(){
        var e int
        ok := true

        for{
            select {
                case e,ok = <- ch11:
                if !ok {
                    fmt.Println("End.")
                    break
                }
                fmt.Printf("ch11 -> %d\n",e)
            }

            //通道關閉後退出for循環
            if !ok {
                sign <- 0
                break
            }
        }

    }()

    //慣用手法,讀取sign通道數據,爲了等待select的Goroutine執行。
    <- sign
}

ch11 -> 0
ch11 -> 1

ch11 -> 999
End.

示例二:加以改進,我們不想等到通道被關閉後再退出循環,利用一個輔助通道模擬出操作超時。

package main

import (
    "fmt"
    "time"
)
func main(){
    //初始化通道
    ch11 := make(chan int, 1000)
    sign := make(chan int, 1)

    //給ch11通道寫入數據
    for i := 0; i < 1000; i++ {
        ch11 <- i
    }
    //關閉ch11通道
    close(ch11)

    //我們不想等到通道被關閉之後再推出循環,我們創建並初始化一個輔助的通道,利用它模擬出操作超時行爲
    timeout := make(chan bool,1)
    go func(){
        time.Sleep(time.Millisecond) //休息1ms
        timeout <- false
    }()

    //單獨起一個Goroutine執行select
    go func(){
        var e int
        ok := true

        for{
            select {
                case e,ok = <- ch11:
                    if !ok {
                        fmt.Println("End.")
                        break
                    }
                    fmt.Printf("ch11 -> %d\n",e)
                case ok = <- timeout:
                //向timeout通道發送元素false後,該case幾乎馬上就會被執行, ok = false
                    fmt.Println("Timeout.")
                    break
            }
            //終止for循環
            if !ok {
                sign <- 0
                break
            }
        }

    }()

    //慣用手法,讀取sign通道數據,爲了等待select的Goroutine執行。
    <- sign
}

ch11 -> 0
ch11 -> 1

ch11 -> 691
Timeout.

示例三:上面實現了單個操作的超時,但是那個超時觸發器開始計時有點早。

package main

import (
    "fmt"
    "time"
)
func main(){
    //初始化通道
    ch11 := make(chan int, 1000)
    sign := make(chan int, 1)

    //給ch11通道寫入數據
    for i := 0; i < 1000; i++ {
        ch11 <- i
    }
    //關閉ch11通道
    //close(ch11),爲了看效果先註釋掉

    //單獨起一個Goroutine執行select
    go func(){
        var e int
        ok := true

        for{
            select {
                case e,ok = <- ch11:
                    if !ok {
                        fmt.Println("End.")
                        break
                    }
                    fmt.Printf("ch11 -> %d\n",e)
                case ok = <- func() chan bool {
                    //經過大約1ms後,該接收語句會從timeout通道接收到一個新元素並賦值給ok,從而恰當地執行了針對單個操作的超時子流程,恰當地結束當前for循環
                    timeout := make(chan bool,1)
                    go func(){
                        time.Sleep(time.Millisecond)//休息1ms
                        timeout <- false
                    }()
                    return timeout
                }():
                    fmt.Println("Timeout.")
                    break
            }
            //終止for循環
            if !ok {
                sign <- 0
                break
            }
        }

    }()

    //慣用手法,讀取sign通道數據,爲了等待select的Goroutine執行。
    <- sign
}

ch11 -> 0
ch11 -> 1

ch11 -> 999
Timeout.

非緩衝的Channel

我們在初始化一個通道時將其容量設置成0,或者直接忽略對容量的設置,那麼就稱之爲非緩衝通道

ch1 := make(chan int, 1) //緩衝通道
ch2 := make(chan int, 0) //非緩衝通道
ch3 := make(chan int) //非緩衝通道
  • 向此類通道發送元素值的操作會被阻塞,直到至少有一個針對該通道的接收操作開始進行爲止。
  • 從此類通道接收元素值的操作會被阻塞,直到至少有一個針對該通道的發送操作開始進行爲止。
  • 針對非緩衝通道的接收操作會在與之相應的發送操作完成之前完成。

對於第三條要特別注意,發送操作在向非緩衝通道發送元素值的時候,會等待能夠接收該元素值的那個接收操作。並且確保該元素值被成功接收,它纔會真正的完成執行。而緩衝通道中,剛好相反,由於元素值的傳遞是異步的,所以發送操作在成功向通道發送元素值之後就會立即結束(它不會關心是否有接收操作)。

示例一

實現多個Goroutine之間的同步

package main

import (
    "fmt"
    "time"
)

func main(){
    unbufChan := make(chan int)
    //unbufChan := make(chan int, 1) 有緩衝容量

    //啓用一個Goroutine接收元素值操作
    go func(){
        fmt.Println("Sleep a second...")
        time.Sleep(time.Second)//休息1s
        num := <- unbufChan //接收unbufChan通道元素值
        fmt.Printf("Received a integer %d.\n", num)
    }()

    num := 1
    fmt.Printf("Send integer %d...\n", num)
    //發送元素值
    unbufChan <- num
    fmt.Println("Done.")
}

緩衝channel輸出結果如下:
Send integer 1…
Done.
======================
非緩衝channel輸出結果如下:
Send integer 1…
Sleep a second…
Received a integer 1.
Done.

在非緩衝Channel中,從打印數據可以看出主Goroutine中的發送操作在等待一個能夠與之配對的接收操作。配對成功後,元素值1才得以經由unbufChan通道被從主Goroutine傳遞至那個新的Goroutine.

select與非緩衝通道

與操作緩衝通道的select相比,它被阻塞的概率一般會大很多。只有存在可配對的操作的時候,傳遞元素值的動作才能真正的開始。
示例:

發送操作間隔1s,接收操作間隔2s
分別向unbufChan通道發送小於10和大於等於10的整數,這樣更容易從打印結果分辨出配對的時候哪一個case被選中了。下列案例兩個case是被隨機選擇的。

package main

import (
    "fmt"
    "time"
)

func main(){
    unbufChan := make(chan int)
    sign := make(chan byte, 2)

    go func(){
        for i := 0; i < 10; i++ {
            select {
                case unbufChan <- i:
                case unbufChan <- i + 10:
                default:
                    fmt.Println("default!")
            }
            time.Sleep(time.Second)
        }
        close(unbufChan)
        fmt.Println("The channel is closed.")
        sign <- 0
    }()

    go func(){
        loop:
            for {
                select {
                    case e, ok := <-unbufChan:
                    if !ok {
                        fmt.Println("Closed channel.")
                        break loop
                    }
                    fmt.Printf("e: %d\n",e)
                    time.Sleep(2 * time.Second)
                }
            }
            sign <- 1
    }()
    <- sign
    <- sign
}

default! //無法配對
e: 1
default!//無法配對
e: 3
default!//無法配對
e: 15
default!//無法配對
e: 17
default!//無法配對
e: 9
The channel is closed.
Closed channel.

default case會在收發操作無法配對的情況下被選中並執行。在這裏它被選中的概率是50%。

  • 上面的示例給予了我們這樣一個啓發:使用非緩衝通道能夠讓我們非常方便地在接收端對發送端的操作頻率實施控制。
  • 可以嘗試去掉default case,看看打印結果,代碼稍作修改如下:
package main

import (
    "fmt"
    "time"
)

func main(){
    unbufChan := make(chan int)
    sign := make(chan byte, 2)

    go func(){
        for i := 0; i < 10; i++ {
            select {
                case unbufChan <- i:
                case unbufChan <- i + 10:

            }
            fmt.Printf("The %d select is selected\n",i)
            time.Sleep(time.Second)
        }
        close(unbufChan)
        fmt.Println("The channel is closed.")
        sign <- 0
    }()

    go func(){
        loop:
            for {
                select {
                    case e, ok := <-unbufChan:
                    if !ok {
                        fmt.Println("Closed channel.")
                        break loop
                    }
                    fmt.Printf("e: %d\n",e)
                    time.Sleep(2 * time.Second)
                }
            }
            sign <- 1
    }()
    <- sign
    <- sign
}

e: 0
The 0 select is selected
e: 11
The 1 select is selected
e: 12
The 2 select is selected
e: 3
The 3 select is selected
e: 14
The 4 select is selected
e: 5
The 5 select is selected
e: 16
The 6 select is selected
e: 17
The 7 select is selected
e: 8
The 8 select is selected
e: 19
The 9 select is selected
The channel is closed.
Closed channel.

總結:上面兩個例子,第一個有default case 無法配對時執行該語句,而第二個沒有default case ,無法配對case時select將阻塞,直到某個case可以運行(上述示例是直到unbufChan數據被讀取操作),不會重新對channel或值進行求值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章