Part 24 Select

歡迎來到 Golang教程系列 的第24節

什麼是 select?

select 語句被用於從多個發送/接收通道操作中選擇。select 語句是阻塞的,直到一個發送/接收操作就緒。如果多個操作準備就緒,將隨機選擇其中的一個。語法和 switch 類似,不同的是每個 case 語句通道操作。讓我們用一些代碼來更好的理解它。

示例

package main

import (  
    "fmt"
    "time"
)

func server1(ch chan string) {  
    time.Sleep(6 * time.Second)
    ch <- "from server1"
}
func server2(ch chan string) {  
    time.Sleep(3 * time.Second)
    ch <- "from server2"

}
func main() {  
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}

上面程序中,第 8 行的 server 函數休眠 6 秒後向通道 ch 寫入文本 from server1。12行的函數 server2休眠 3 秒後 向通道 ch 定稿 from server2

主函數分別在 20 行和 21 行調用go協程 server1server2

22 行,控制到達 select 語句。select 語句阻塞,直到它的一個 case 準備就緒。在我們上面的程序中, server1 協程休眠 6 秒後向通道 output1 寫入數據,而 server2 在 3 秒後寫入 output2 通道。所以 select 語句將阻塞 3 秒,等待 server2 協程寫入output2通道,在 3 秒後,程序打印

from server2

然後終止。

實際使用 select

在上面程序命名函數 serverserver2 是爲了練習使用 select

我們假設有一個關鍵任務應用程序,我們需要儘快地將輸出返回給用戶。這個應用程序的數據庫被複制並存儲在世界的不同服務器中。假設函數 server1server2 實際上與 2 個這樣的服務器通信。每個服務的響應時間取決於每個的負載以及網絡延遲。我們向這兩個服務器發送請求,然後使用 select 語句在相應的通道上等待響應。第一個響應的服務器被 select 選擇,而另一個被忽略。這樣我們可以向多個服務器發送相同的請求並將響應最快的返回給用戶。

default case

select 語句中,當沒有任何 case 準備就緒,將執行 default case。這通常用來防止 select 語句阻塞。

package main

import (  
    "fmt"
    "time"
)

func process(ch chan string) {  
    time.Sleep(10500 * time.Millisecond)
    ch <- "process successful"
}

func main() {  
    ch := make(chan string)
    go process(ch)
    for {
        time.Sleep(1000 * time.Millisecond)
        select {
        case v := <-ch:
            fmt.Println("received value: ", v)
            return
        default:
            fmt.Println("no value received")
        }
    }

}

上面程序的 8 行,函數 process 休眠 10500 毫秒(10.5秒),然後將 process successful 寫入 ch 通道。這個函數在這個程序的 15 行被併發地調用 。

在併發地調用 process 協程後,在主協程中啓動一個 for 死循環。該 for 循環在每次迭代開始時休眠 1000毫秒(10秒),並執行 select 操作。在第一個 10500 毫秒,select 語句的第一個名爲 case v := <-ch: 的 case 將未準備好,因爲 process 協程僅在 10500毫秒後寫入 ch 通道。因此,default case 在這個過程中被執行,程序將打印 10 次 no value received

在 10.5 秒,在 10 行,協程 processch 通道寫入 process successfu l。現在 select 語句的第一個 case 被執行,程序將打印 received value: process successful ,然後它將終止,這個程序將輸出。

no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
no value received  
received value:  process successful  

死鎖(deadlock)和 default case

package main

func main(){
	ch := make(chan string)
	select {
	case <- ch:
	}
}

在上面程序,我們已經在第 4 行創建一個通道 ch 。我們在 6 行的select 裏面讀該通道。通道將永遠阻塞 ,因爲沒有其他協程向該通道寫入,因此將導致死鎖。該程序將在運行時使用下列消息 panic。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/tmp/sandbox416567824/main.go:6 +0x80

如果有個默認分支,這個死鎖將不會發生,因爲當沒有 case 準備就緒時,將執行默認 case。上面的程序將被使用 default case 重寫,如下

package main

import "fmt"

func main() {  
    ch := make(chan string)
    select {
    case <-ch:
    default:
        fmt.Println("default case executed")
    }
}

上面的程序將打印

default case executed

類似地,即使 select 僅有一個 nil 通道,默認 case 也將被執行

package main

import "fmt"

func main() {  
    var ch chan string
    select {
    case v := <-ch:
        fmt.Println("received value", v)
    default:
        fmt.Println("default case executed")

    }
}

上面程序中 chnil。我們在 8 行的 select 中試圖讀 ch。如果沒有 defaultselect 將被永遠阻塞並導致死鎖。由於我們在 select 中已經有一個 default,它將被執行,程序打印,

default case executed

隨機選擇

當一個 select 語句中的多個 case 準備就緒,它們中的一個將被隨機執行。

package main

import (  
    "fmt"
    "time"
)

func server1(ch chan string) {  
    ch <- "from server1"
}
func server2(ch chan string) {  
    ch <- "from server2"

}
func main() {  
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    time.Sleep(1 * time.Second)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    }
}

在上面程序中,go協程 server1server2 分別在 18 和 19 行被調用。然後主協程在 20 行休眠 1 秒。當執行到21 行的 select 語句,server1 已經向通道 output 寫入 from serverserver2 已經向通道 output2 寫入 from server2.因此 select 語句的兩個通道都已準備就緒待執行。如果多次運行該程序,輸出將會在 from server1server2 之間變化,取決於哪一個 case 被隨機選擇。

請在你本地系統上運行該程序來獲取這個隨機性。如果這個程序運行在 playground,它將打印同樣的輸出,因爲 playground 是確定性的。

疑難雜症 - 空 select

package main

func main(){
	select {}
}

上面的程序會輸出什麼 ?
我們知道 select 語句將阻塞直到它的一個 case 被執行。在這個示例中,select 語句沒有任何 case,因此它將永遠阻塞並導致死鎖。這個程序將以下面的輸出拋出 panic,

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:  
main.main()  
    /tmp/sandbox299546399/main.go:4 +0x20

**下一教程 - Mutex **

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