gopl-zh.github.com/ch8/ch8-07.md
2015-12-30 20:16:09 +08:00

6.5 KiB
Raw Blame History

8.7. 基於select的多路複用

下面的程序会进行火箭发射的倒计时。time.Tick函数返回一个channel程序会周期性地像一个节拍器一样向这个channel发送事件。每一个事件的值是一个时间戳 but it is rarely as interesting as the fact of its delivery.(这句实在不会翻译。。)

gopl.io/ch8/countdown1
func main() {
    fmt.Println("Commencing countdown.")
    tick := time.Tick(1 * time.Second)
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        j<-tick
    }
    launch()
}

现在我们让这个程序支持在倒计时中用户按下return键时直接中断发射流程。首先我们启动一个goroutine这个goroutine会尝试从标准输入中调入一个单独的byte并且如果成功了会向名为abort的channel发送一个值。

gopl.io/ch8/countdown2
abort := make(chan struct{})
go func() {
    os.Stdin.Read(make([]byte, 1)) // read a single byte
    abort <- struct{}{}
}()

现在每一次计数循环的迭代都需要等待两个channel中的其中一个返回事件了ticker channel当一切正常时(就像NASA jorgon的"nominal",译注:这梗估计我们是不懂了)或者异常时返回的abort事件。我们无法做到从每一个channel中接收信息如果我们这么做的话如果第一个channel中没有事件发过来那么程序就会立刻被阻塞这样我们就无法收到第二个channel中发过来的事件。这时候我们需要多路复用(multiplex)这些操作了为了能够多路复用我们使用了select语句。

select {
case <-ch1:
    // ...
case x := <-ch2:
    // ...use x...
case ch3 <- y:
    // ...
default:
    // ...
}

The general form of a select statement is shown above. Like a switch statement, it has a num- ber of cases and an optional default. Each case specifies a communication (a send or receive operation on some channel) and an associated block of statements. A receive expression may appear on its own, as in the first case, or within a short variable declaration, as in the second case; the second form lets you refer to the received value. A select waits until a communication for some case is ready to proceed. It then performs that communication and executes the cases associated statements; the other communications do not happen. A select with no cases, select{}, waits forever. select会等待case中有能够执行的case时去执行。当条件满足时select才会去通信并执行case之后的语句 Lets return to our rocket launch program. The time.After function immediately returns a channel, and starts a new goroutine that sends a single value on that channel after the speci- fied time. The select statement below waits until the first of two events arrives, either an abort event or the event indicating that 10 seconds have elapsed. If 10 seconds go by with no abort, the launch proceeds.

func main() {
    // ...create abort channel...

    fmt.Println("Commencing countdown.  Press return to abort.")
    select {
    case <-time.After(10 * time.Second):
        // Do nothing.
    case <-abort:
        fmt.Println("Launch aborted!")
        return
    }
    launch()

下面这个例子更微秒。ch这个channel的buffer大小是1所以会交替的为空或为满所以只有一个case可以进行下去无论i是奇数或者偶数它都会打印0 2 4 6 8。

ch := make(chan int, 1)
for i := 0; i < 10; i++ {
    select {
    case x := <-ch:
        fmt.Println(x) // "0" "2" "4" "6" "8"
    case ch <- i:
    }
}

如果多个case同时就绪时select会随机地选择一个执行这样来保证每一个channel都有平等的被select的机会。增加前一个例子的buffer大小会使其输出变得不确定因为当buffer既不为满也不为空时select语句的执行情况就像是抛硬币的行为一样是随机的。 下面让我们的发射程序打印倒计时。这里的select语句会使每次循环迭代等待一秒来执行退出操作。

gopl.io/ch8/countdown3
func main() {
    // ...create abort channel...

    fmt.Println("Commencing countdown.  Press return to abort.")
    tick := time.Tick(1 * time.Second)
    for countdown := 10; countdown > 0; countdown-- {
        fmt.Println(countdown)
        select {
        case <-tick:
            // Do nothing.
        case <-abort:
            fmt.Println("Launch aborted!")
            return
        }
    }
    launch()
}

The time.Tick function behaves as if it creates a goroutine that calls time.Sleep in a loop, sending an event each time it wakes up. When the countdown function above returns, it stops receiving events from tick, but the ticker goroutine is still there, trying in vain to send on a channel from which no goroutine is receiving—a goroutine leak (§8.4.4). Tick函数挺方便但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话我们应该使用下面的这种模式

ticker := time.NewTicker(1 * time.Second)
<-ticker.C // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate

有时候我们希望能够从channel中发送或者接收值并避免因为发送或者接收导致的阻塞尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。 下面的select语句会在abort channel中有值时从其中接收值无值时什么都不做。这是一个非阻塞的接收操作反复地做这样的操作叫做“轮询channel”。

select {
case <-abort:
    fmt.Printf("Launch aborted!\n")
    return
default:
    // do nothing
}

channel的零值是nil。也许会让你觉得比较奇怪nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞在select语句中操作nil的channel永远都不会被select到。 This lets us use nil to enable or disable cases that cor- respond to features like handling timeouts or cancellation, responding to other input events, or emitting output. Well see an example in the next section. 练习8.8: 使用select来改造8.3节中的echo服务器为其增加超时这样服务器可以在客户端10秒中没有任何喊话时自动断开连接。