Goroutines
Goroutine是Go中最基本的執行單元。事實上每一個Go程式至少有一個goroutine:主goroutine。當程式啟動時,它會自動建立。
事實上goroutine採用了一種fork-join的模型。
sayHello := func() {
fmt.Println("hello")
}
go sayHello()
複製程式碼
那我們如何來join goroutine呢?需要引入wait操作:
var wg sync.WaitGroup()
sayHello := func() {
defer wg.Done()
fmt.Println("hello")
}
wg.Add(1)
go sayHello()
wa.Wait()
複製程式碼
Channel
讀寫channel
goroutine是Go語言的基本排程單位,而channels則是它們之間的通訊機制。操作符<-用來指定管道的方向,傳送或接收。如果未指定方向,則為雙向管道。
// 建立一個雙向channel
ch := make(chan interface{})
複製程式碼
interface{}表示chan可以為任何型別
channel有傳送和接受兩個主要操作。傳送和接收兩個操作都使用<-運算子。在傳送語句中,channel放<-運算子左邊。在接收語句中,channel放<-運算子右邊。一個不使用接收結果的接收操作也是合法的。
// 傳送操作
ch <- x
// 接收操作
x = <-ch
// 忽略接收到的值,合法
<-ch
複製程式碼
我們不能弄錯channel的方向:
writeStream := make(chan<- interface{})
readStream := make(<-chan interface{})
<-writeStream
readStream <- struct{}{}
複製程式碼
上面的語句會產生如下錯誤:
invalid operation: <-writeStream (receive from send-only type chan<- interface {}) invalid operation: readStream <- struct {} literal (send to receive-only type <-chan interface {})
複製程式碼
關閉channel
Channel支援close操作,用於關閉channel,後面對該channel的任何傳送操作都將導致panic異常。對一個已經被close過的channel進行接收操作依然可以接受到之前已經成功傳送的資料;如果channel中已經沒有資料的話將產生一個零值的資料。
從已經關閉的channel中讀:
intStream := make(chan int)
close(intStream)
integer, ok := <- intStream
fmt.Pritf("(%v): %v", ok, integer)
// (false): 0
複製程式碼
上面例子中通過返回值ok來判斷channel是否關閉,我們還可以通過range這種更優雅的方式來處理已經關閉的channel:
intStream := make(chan int)
go func() {
defer close(intStream)
for i:=1; i<=5; i++{
intStream <- i
}
}()
for integer := range intStream {
fmt.Printf("%v ", integer)
}
// 1 2 3 4 5
複製程式碼
帶緩衝(buffered)的channel
建立了一個可以持有三個字串元素的帶緩衝Channel:
ch = make(chan string, 3)
複製程式碼
我們可以在無阻塞的情況下連續向新建立的channel傳送三個值:
ch <- "A"
ch <- "B"
ch <- "C"
複製程式碼
此刻,channel的內部緩衝佇列將是滿的,如果有第四個傳送操作將發生阻塞。
如果我們接收一個值:
fmt.Println(<-ch) // "A"
複製程式碼
那麼channel的緩衝佇列將不是滿的也不是空的,因此對該channel執行的傳送或接收操作都不會發生阻塞。通過這種方式,channel的緩衝佇列緩衝解耦了接收和傳送的goroutine。
帶緩衝的通道可被用作訊號量,例如限制吞吐量。在此例中,進入的請求會被傳遞給 handle,它從通道中接收值,處理請求後將值發回該通道中,以便讓該 “訊號量” 準備迎接下一次請求。通道緩衝區的容量決定了同時呼叫 process 的數量上限。
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // 等待活動佇列清空。
process(r) // 可能需要很長時間。
<-sem // 完成;使下一個請求可以執行。
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // 無需等待 handle 結束。
}
}
複製程式碼
然而,它卻有個設計問題:儘管只有 MaxOutstanding 個 goroutine 能同時執行,但 Serve 還是為每個進入的請求都建立了新的 goroutine。其結果就是,若請求來得很快, 該程式就會無限地消耗資源。為了彌補這種不足,我們可以通過修改 Serve 來限制建立 Go 程,這是個明顯的解決方案,但要當心我們修復後出現的 Bug。
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // 這兒有 Bug,解釋見下。
<-sem
}()
}
}
複製程式碼
Bug 出現在 Go 的 for 迴圈中,該迴圈變數在每次迭代時會被重用,因此 req 變數會在所有的 goroutine 間共享,這不是我們想要的。我們需要確保 req 對於每個 goroutine 來說都是唯一的。有一種方法能夠做到,就是將 req 的值作為實參傳入到該 goroutine 的閉包中:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
複製程式碼
另一種解決方案就是以相同的名字建立新的變數,如例中所示:
func Serve(queue chan *Request) {
for req := range queue {
req := req // 為該 Go 程建立 req 的新例項。
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
複製程式碼
下面再看一個Go語言聖經的例子。它併發地向三個映象站點發出請求,三個映象站點分散在不同的地理位置。它們分別將收到的響應傳送到帶緩衝channel,最後接收者只接收第一個收到的響應,也就是最快的那個響應。因此mirroredQuery函式可能在另外兩個響應慢的映象站點響應之前就返回了結果。
func mirroredQuery() string {
responses := make(chan string, 3)
go func() { responses <- request("asia.gopl.io") }()
go func() { responses <- request("europe.gopl.io") }()
go func() { responses <- request("americas.gopl.io") }()
// 僅僅返回最快的那個response
return <-responses
}
func request(hostname string) (response string) { /* ... */ }
複製程式碼
如果我們使用了無緩衝的channel,那麼兩個慢的goroutines將會因為沒有人接收而被永遠卡住。這種情況,稱為goroutines洩漏,這將是一個BUG。和垃圾變數不同,洩漏的goroutines並不會被自動回收,因此確保每個不再需要的goroutine能正常退出是重要的。
Channels of channels
Go 最重要的特性就是通道是first-class value,它可以被分配並像其它值到處傳遞。 這種特性通常被用來實現安全、並行的多路分解。
我們可以利用這個特性來實現一個簡單的RPC。 以下為 Request 型別的大概定義。
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
複製程式碼
客戶端提供了一個函式及其實參,此外在請求物件中還有個接收應答的通道。
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// 傳送請求
clientRequests <- request
// 等待迴應
fmt.Printf("answer: %d\n", <-request.resultChan)
複製程式碼
服務端的handler函式:
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
複製程式碼
Channels pipeline
Channels也可以用於將多個goroutine連線在一起,一個Channel的輸出作為下一個Channel的輸入。這種串聯的Channels就是所謂的管道(pipeline)。下面的程式用兩個channels將三個goroutine串聯起來:
第一個goroutine是一個計數器,用於生成0、1、2、……形式的整數序列,然後通過channel將該整數序列傳送給第二個goroutine;第二個goroutine是一個求平方的程式,對收到的每個整數求平方,然後將平方後的結果通過第二個channel傳送給第三個goroutine;第三個goroutine是一個列印程式,列印收到的每個整數。
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
複製程式碼
Select多路複用
select用於從一組可能的通訊中選擇一個進一步處理。如果任意一個通訊都可以進一步處理,則從中隨機選擇一個,執行對應的語句。否則,如果又沒有預設分支(default case),select語句則會阻塞,直到其中一個通訊完成。
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
複製程式碼
如何使用select語句為一個操作設定一個時間限制。程式碼會輸出變數news的值或者超時訊息,具體依賴於兩個接收語句哪個先執行:
select {
case news := <-NewsAgency:
fmt.Println(news)
case <-time.After(time.Minute):
fmt.Println("Time out: no news in one minute.")
}
複製程式碼
下面的select語句會在abort channel中有值時,從其中接收值;無值時什麼都不做。這是一個非阻塞的接收操作;反覆地做這樣的操作叫做“輪詢channel”。
select {
case <-abort:
fmt.Printf("Launch aborted!\n")
return
default:
// do nothing
}
複製程式碼
參考資料。