[譯] part24: golang select

咔嘰咔嘰發表於2019-04-03

什麼是 select

select語句用於從多個傳送/接收channel中進行選擇的操作。 select語句將阻塞直到其中一個傳送/接收操作準備就緒。如果有多個操作就緒,則隨機選擇其中一個操作。語法類似於switch,只是每個case語句被一個channel操作取代了。讓我們深入研究一些程式碼,以便更好地理解

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)
    }
}
複製程式碼

Run in playgroud

在上面的程式中,在第 8 行server1函式休眠 6 秒然後將文字從server1寫入channel ch。第 12 行server2函式休眠 3 秒,然後從server2寫入channel ch

main函式在 20 和 21 行分別呼叫server1server2

在第 22 行,select語句將阻塞直到其中一個case準備就緒。在上面的程式中,server1在 6 秒後寫入output1 channel,而server2在 3 秒後寫入output2 channel。因此 select 語句將阻塞 3 秒並等待server2寫入。 3 秒後,程式將列印,

from server2
複製程式碼

然後終止。

select 的用途

將上述程式中的函式命名為server1server2的原因是為了說明select的實際用途。

讓我們假設我們有一個關鍵任務的應用,我們需要儘快將輸出返回給使用者。該應用程式的資料庫被複制並儲存在世界各地的不同伺服器中。假設函式server1server2實際上與 2 個這樣的伺服器通訊。每個伺服器的響應時間取決於每個伺服器的負載和網路延遲。我們將請求傳送到兩個伺服器,然後使用select語句在相應的channel上等待響應。select會選擇優先響應的伺服器,其他響應被忽略。這樣我們就可以向多個伺服器傳送相同的請求,並將最快的響應返回給使用者:)。

預設case

當其他case都沒有準備就緒時,將會執行select語句中的預設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")
        }
    }

}
複製程式碼

Run in playground

在上面的程式中,在第 8 行process函式休眠 10500 毫秒(10.5 秒),然後將process successful寫入ch channel。該函式在第 15 行被併發呼叫。

在併發呼叫process Goroutine之後,main Goroutine中啟動了無限迴圈。無限迴圈在每次迭代開始期間休眠 1000 毫秒(1 秒),並執行select操作。在前 10500 毫秒期間,select語句的第一種情況即case v:= <-ch:將不會準備就緒,因為process Goroutine僅在 10500 毫秒後才寫入ch channel。因此,在此期間將執行defualt分支,程式將會列印 10 次no value received

在 10.5 秒之後,process Goroutineprocess successful寫入ch。 現在將執行select語句的第一種情況,程式將列印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
複製程式碼

死鎖和預設case

package main

func main() {
    ch := make(chan string)
    select {
    case <-ch:
    }
}
複製程式碼

Run in playgroud

在上面的程式中,我們在第一行建立了一個channel ch。我們嘗試從選擇的這個channel讀取。而這個select語句將一直阻塞,因為沒有其他Goroutine寫入此channel,因此將導致死鎖。該程式將在執行時產生panic同時列印,

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /tmp/sandbox416567824/main.go:6 +0x80
複製程式碼

如果存在預設case,則不會發生此死鎖,因為在沒有其他case準備就緒時將執行預設case。上面的程式可以重寫。

package main

import "fmt"

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

Run in playground

輸出,

default case executed
複製程式碼

類似地,當select只有一個nil channel,也會執行預設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")

    }
}
複製程式碼

Run in playground

在上面的程式中,chnil,我們試圖用selectch中讀取。如果沒有預設case,則select將一直被阻塞並導致死鎖。由於我們在select中有一個預設的case,它將被執行並且程式將列印,

default case executed
複製程式碼

select的隨機性

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)
    }
}
複製程式碼

Run in playground

在上面的程式中,server1server2 協程在第 18 和 19 行分別被呼叫,然後main協程休眠 1 秒。當執行到select語句時,server1已將from server1寫入output1server2已將from server2寫入output2,因此select語句中的兩種情況都準備就緒。如果多次執行此程式,將會隨機輸出from server1from server2

select

package main

func main() {
    select {}
}
複製程式碼

Run in playground

你認為上面的程式將會輸出什麼?

我們知道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
複製程式碼

相關文章