14.4 使用 select 切換協程

從不同的併發執行的協程中獲取值可以通過關鍵字select來完成,它和switch控制語句非常相似(章節5.3)也被稱作通信開關;它的行爲像是“你準備好了嗎”的輪詢機制;select監聽進入通道的數據,也可以是用通道發送值的時候。

select {
case u:= <- ch1:
        ...
case v:= <- ch2:
        ...
        ...
default: // no value ready to be received
        ...
}

default 語句是可選的;fallthrough 行爲,和普通的 switch 相似,是不允許的。在任何一個 case 中執行 break 或者 return,select 就結束了。

select 做的就是:選擇處理列出的多個通信情況中的一個。

  • 如果都阻塞了,會等待直到其中一個可以處理
  • 如果多個可以處理,隨機選擇一個
  • 如果沒有通道操作可以處理並且寫了 default 語句,它就會執行:default 永遠是可運行的(這就是準備好了,可以執行)。

select 中使用發送操作並且有 default可以確保發送不被阻塞!如果沒有 case,select 就會一直阻塞。

select 語句實現了一種監聽模式,通常用在(無限)循環中;在某種情況下,通過 break 語句使循環退出。

在程序 goroutine_select.go 中有 2 個通道 ch1ch2,三個協程 pump1()pump2()suck()。這是一個典型的生產者消費者模式。在無限循環中,ch1ch2 通過 pump1()pump2() 填充整數;suck() 也是在無限循環中輪詢輸入的,通過 select 語句獲取 ch1ch2 的整數並輸出。選擇哪一個 case 取決於哪一個通道收到了信息。程序在 main 執行 1 秒後結束。

示例 14.10-goroutine_select.go

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go pump1(ch1)
    go pump2(ch2)
    go suck(ch1, ch2)

    time.Sleep(1e9)
}

func pump1(ch chan int) {
    for i := 0; ; i++ {
        ch <- i * 2
    }
}

func pump2(ch chan int) {
    for i := 0; ; i++ {
        ch <- i + 5
    }
}

func suck(ch1, ch2 chan int) {
    for {
        select {
        case v := <-ch1:
            fmt.Printf("Received on channel 1: %d\n", v)
        case v := <-ch2:
            fmt.Printf("Received on channel 2: %d\n", v)
        }
    }
}

輸出:

Received on channel 2: 5
Received on channel 2: 6
Received on channel 1: 0
Received on channel 2: 7
Received on channel 2: 8
Received on channel 2: 9
Received on channel 2: 10
Received on channel 1: 2
Received on channel 2: 11
...
Received on channel 2: 47404
Received on channel 1: 94346
Received on channel 1: 94348

一秒內的輸出非常驚人,如果我們給它計數(goroutine_select2.go),得到了 90000 個左右的數字。

練習:

練習 14.7:

  • a)在練習 5.4 的 for_loop.go 中,有一個常見的 for 循環打印數字。在函數 tel 中實現一個 for 循環,用協程開始這個函數並在其中給通道發送數字。main() 線程從通道中獲取並打印。不要使用 time.Sleep() 來同步:goroutine_panic.go
  • b)也許你的方案有效,可能會引發運行時的 panic:throw:all goroutines are asleep-deadlock! 爲什麼會這樣?你如何解決這個問題?goroutine_close.go
  • c)解決 a)的另外一種方式:使用一個額外的通道傳遞給協程,然後在結束的時候隨便放點什麼進去。main() 線程檢查是否有數據發送給了這個通道,如果有就停止:goroutine_select.go

練習 14.8:

從示例 6.10 的斐波那契程序開始,制定解決方案,使斐波那契週期計算獨立到協程中,並可以把結果發送給通道。

結束的時候關閉通道。main() 函數讀取通道並打印結果:goFibonacci.go

使用練習 6.9 中的算法寫一個更短的 gofibonacci2.go

使用 select 語句來寫,並讓通道退出(gofibonacci_select.go

注意:當給結果計時並和 6.10 對比時,我們發現使用通道通信的性能開銷有輕微削減;這個例子中的算法使用協程並非性能最好的選擇;但是 gofibonacci3 方案使用了 2 個協程帶來了 3 倍的提速。

練習 14.9:

做一個隨機位生成器,程序可以提供無限的隨機 0 或者 1 的序列:random_bitgen.go

練習 14.10:polar_to_cartesian.go

(這是一種綜合練習,使用到第 4、9、11 章和本章的內容。)寫一個可交互的控制檯程序,要求用戶輸入二位平面極座標上的點(半徑和角度(度))。計算對應的笛卡爾座標系的點的 x 和 y 並輸出。使用極座標和笛卡爾座標的結構體。

使用通道和協程:

  • channel1 用來接收極座標
  • channel2 用來接收笛卡爾座標

轉換過程需要在協程中進行,從 channel1 中讀取然後發送到 channel2。實際上做這種計算不提倡使用協程和通道,但是如果運算量很大很耗時,這種方案設計就非常合適了。

練習 14.11: concurrent_pi.go / concurrent_pi2.go

使用以下序列在協程中計算 pi:開啓一個協程來計算公式中的每一項並將結果放入通道,main() 函數收集並累加結果,打印出 pi 的近似值。

計算執行時間(參見第 6.11 節)

再次聲明這只是爲了一邊練習協程的概念一邊找點樂子。

如果你需要的話可使用 math.pi 中的 Pi;而且不使用協程會運算的更快。一個急速版本:使用 GOMAXPROCS,開啓和 GOMAXPROCS 同樣多個協程。

習慣用法:後臺服務模式

服務通常是是用後臺協程中的無限循環實現的,在循環中使用 select 獲取並處理通道中的數據:

// Backend goroutine.
func backend() {
    for {
        select {
        case cmd := <-ch1:
            // Handle ...
        case cmd := <-ch2:
            ...
        case cmd := <-chStop:
            // stop server
        }
    }
}

在程序的其他地方給通道 ch1ch2 發送數據,比如:通道 stop 用來清理結束服務程序。

另一種方式(但是不太靈活)就是(客戶端)在 chRequest 上提交請求,後臺協程循環這個通道,使用 switch 根據請求的行爲來分別處理:

func backend() {
    for req := range chRequest {
        switch req.Subjext() {
            case A1:  // Handle case ...
            case A2:  // Handle case ...
            default:
              // Handle illegal request ..
              // ...
        }
    }
}

鏈接

results matching ""

    No results matching ""