阅读 757

[译] part24: golang select

什么是 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
复制代码
文章分类
后端
文章标签