【Go語言】【18】GO語言的select

1、selectgit

Go語言引入了select關鍵字,其語法與switch很是相似,先看一個switch例子:
編程

func main() {併發

        var a int = 1ide

        switch {函數

                case a == 1:ui

                        fmt.Println("ok")lua

                case a == 2:spa

                        fmt.Println("no ok")命令行

                default:3d

                        fmt.Println("default")

        }

}

運行該程序,能夠正常打印出「ok」

下面把switch替換爲select

func main() {

        var a int = 1

        select{

                case a == 1:

                        fmt.Println("ok")

                case a == 2:

                        fmt.Println("no ok")

                default:

                        fmt.Println("default")

        }

}

運行該程序拋出錯誤:a == 1 evaluated but not used

這是爲什麼?是由於select中的case語句必須是一個IO操做!!!

即:switch中的case語句判斷條件只要能比較就行,而select中的case語句判斷條件必須是一個IO操做。


咱們在《Go語言的併發》中說過Channel,從Channel中讀取值和向Channel中寫入值對應的操做都是IO操做。下面咱們寫一個關於select的例子:

func setValue(ch1 chan int, ch2 chan string) {

        ch1 <- 1

        ch2 <- "goroutine"

}

       定義一個函數setValue(),入參爲兩個channel,該方法體內向channel 1中寫入一個×××值、向channel 2中寫入一個字符串;在併發章節咱們說過:「當入覺得channel時,就不是值傳遞了,而變成一個地址傳遞」。

func main() {

        var ch1 chan int = make(chan int)

        var ch2 chan string = make(chan string)

        go setValue(ch1, ch2)


        select {

                  case <-ch1:

                          fmt.Println("ch1 ok")

                  case <-ch2:

                          fmt.Println("ch2 ok")

                  default:

                          fmt.Println("default")

        }

}

        先定義兩個channel類型的變量,使用make對其初始化;而後使用go關鍵字拉啓一個goroutine;main所在goroutine繼續向下走,開始執行select。

執行一下程序:

wKioL1XGzffjzGR0AACJ40MJD08385.jpg

發現只打印了一個「default」,而沒有打印「ch1 ok」或者「ch2 ok」,多執行幾回結果同樣,從併發的角度上考慮機率狀況,這是不正常的。


您可能會想向ch一、ch2中寫入數據,屬於IO操做,可能會慢一些,當select執行完default時,IO操做依舊沒有完成。咱們驗證一下,修改代碼:

func main() {

        var ch1 chan int = make(chan int)

        var ch2 chan string = make(chan string)

        go setValue(ch1, ch2)


        select {

                  case <-ch1:

                          fmt.Println("ch1 ok")

                  case <-ch2:

                          fmt.Println("ch2 ok")

        }

}

這裏把default刪除掉了,當執行到select時,程序會查看各個分支,因爲沒有default分支,此時若channel中沒有內容,則main所在的goroutin會阻塞,至到ch1或者ch2中有內容爲至。


執行一下該程序:

wKioL1XG0PPSUmstAAC84L3mw_g272.jpg

發現成功打印出"ch1 ok」,多運行幾回依舊打印出「ch1 ok」,從沒有打印出「ch2 ok」,這是爲何呢?


看過我前面章節的可能會想,因爲目前使用的是go1.4版本,它並不支持多核併發,加之GO語言在執行時更傾向先讓一個goroutine執行完(即咱們常說的讓領導先走:) ),下面咱們再修改一下程序:

func main() {

        runtime.GOMAXPROCS(runtime.NumCPU())  // 強制Go進行多核併發


        var ch1 chan int = make(chan int)

        var ch2 chan string = make(chan string)

        go setValue(ch1, ch2)


        select {

                  case <-ch1:

                          fmt.Println("ch1 ok")

                  case <-ch2:

                          fmt.Println("ch2 ok")

        }

}

再多運行幾回程序:

wKioL1XG1VOQ0tMyAAEJlkJS3v4955.jpg


從運行結果上來看,多核也沒有解決這個問題,這是由於當執行到select時,它發現ch一、ch2均無內容,程序發生阻塞,直到另外的goroutine把數據寫入ch1或ch2,因爲在另外的goroutine中ch1老是第一個被寫入數據,因此main所在的gorouinte老是先從ch1中獲取到數據,從而打印「ch1 ok」以後就退出了!


那如何完善這個程序呢?

func setValue(ch chan int){

         ch <- 1

}


func main() {

        runtime.GOMAXPROCS(runtime.NumCPU())


        var ch1, ch2 chan int = make(chan int), make(chan int)

        go setValue(ch1)

        go setValue(ch2)


        select {

                case <-ch1:

                        fmt.Println("ch1 ok")

                case <-ch2:

                        fmt.Println("ch2 ok")

        }

}       

告訴Go的運行環境當前是多核編程,同時起2個goroutine在多核的狀態下運行,這樣向channel一、channel2中寫入數據順序就變得不可預測了因此有可能先打印「ch1 ok」,也有可能先打印「ch2 ok」。

多運行幾回看一下結果:

wKiom1XR6FXBCYTuAADsARgrzTU715.jpg

從結果上來看,達到了預期目的!


2、死鎖

       如上面的setValue()方法所示,入參是一個channel,方法體是向該channel中寫入數據,因爲channel做爲入參是一個地址傳遞,因此在select中的case始終能從channel中讀取到數據。

       試想若程序猿把setValue()中賦值忘記了呢?以下:

func setValue(ch chan int) {

        // ch <- 1  註釋掉該行

}

運行一下發現系統報了死鎖:

wKioL1XR7I7g1U1dAADZswIIZPI327.jpg


像這種狀況是在所不免的,若是避免死鎖這類問題呢?

有一種辦法是引入另外一個超時Channel,另啓一個goroutine先讓它休息必定時間(超時時間),而後把數據寫入該Channel,代碼以下:

package main


import (

        "fmt"

        "runtime"

        "time"

)


func setValue(ch chan int) {

        //ch <- 1  讓ch一、ch2產生死鎖

}


func main() {

runtime.GOMAXPROCS(runtime.NumCPU())

var timeout chan bool = make(chan bool)  // 建立一個超時的Channel

go func() {                                                    // 新建立一個goroutine

time.Sleep(time.Second * 10)              // 休息10秒

timeout <- true                                   // 在10秒內ch一、ch2若尚未向裏面寫入數據,則認爲超時

}()                                                                 // 加一個()的意思是讓這個gorouinte執行


var ch1, ch2 chan int = make(chan int), make(chan int)

go setValue(ch1)

go setValue(ch2)


select {

case <-ch1:

fmt.Println("ch1 ok")

case <-ch2:

fmt.Println("ch2 ok")

case <-timeout:                                        // 若ch一、ch2死鎖,10秒鐘後timeout填充數據,避免死鎖

fmt.Println("Timeout coming...")


}

}

運行一下程序:

wKioL1XR8WiRTShTAACVLbV5LVU524.jpg


3、有意思的程序

啥話都別說了,直接上代碼:

package main


import (

        "fmt"

)


func main() {

        var ch chan int = make(chan int, 1)


        for {

                select {

                          case ch <- 0:

                          case ch <- 1:

                }


                fmt.Printf("%d", <-ch)

        }

}

看懂了沒有?

解釋一下:

一、先定義一個類型爲int的Channel

二、再來一個死循環

三、使用select關鍵字進行選擇

四、case的後面是分別向ch寫入0或者1

五、使用Printf進行打印結果

在命令行窗口或者git中執行一下該程序,結果以下:

wKiom1XR8lPgtZLbAAZCr9Pecis232.jpg

打印出一連串的隨機數


這是爲何呢?

select {

        case ch <- 0:

        case ch <- 1:

}

這句話的意思就是向channel中放置數據爲0或者1的隨機數.

相關文章
相關標籤/搜索