這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
channel 是 golang 裡相當有趣的一個功能,大部分時候 channel 都是和 goroutine 一起配合使用。本文主要介紹 channel 的一些有趣的用法。
通道(channel)
,像是通道(管道),可以通過它們發送類型化的資料在協程之間通訊,可以避開所有記憶體共用導致的坑;通道的通訊方式保證了同步性。資料通過通道:同一時間只有一個協程可以訪問資料:所以不會出現資料競爭,設計如此。資料的歸屬(可以讀寫資料的能力)被傳遞。
通道實際上是類型化訊息的隊列:使資料得以傳輸。它是先進先出(FIFO)結構的所以可以保證發送給他們的元素的順序(有些人知道,通道可以比作 Unix shells 中的雙向管道(tw-way pipe))。通道也是參考型別,所以我們使用 make()
函數來給它分配記憶體。
二、Go Channel基本操作文法
Go Channel的基本操作文法如下:
c := make(chan bool) //建立一個無緩衝的bool型Channel
c <- x //向一個Channel發送一個值
<- c //從一個Channel中接收一個值
x = <- c //從Channel c接收一個值並將其儲存到x中
x, ok = <- c //從Channel接收一個值,如果channel關閉了或沒有資料,那麼ok將被置為false
預設情況下,通訊是同步且無緩衝的:在有接受者接收資料之前,發送不會結束。可以想象一個無緩衝的通道在沒有空間來儲存資料的時候:必須要一個接收者準備好接收通道的資料然後寄件者可以直接把資料發送給接收者。所以通道的發送/接收操作在對方準備好之前是阻塞的:
1)對於同一個通道,發送操作(協程或者函數中的),在接收者準備好之前是阻塞的:如果ch中的資料無人接收,就無法再給通道傳入其他資料:新的輸入無法在通道非空的情況下傳入。所以發送操作會等待 ch 再次變為可用狀態:就是通道值被接收時(可以傳入變數)。
2)對於同一個通道,接收操作是阻塞的(協程或函數中的),直到寄件者可用:如果通道中沒有資料,接收者就阻塞了。
三、Channel用作訊號(Signal)的情境(訊號量)
1、等待一個事件(Event)
等待一個事件。例如:
package main
import "fmt"
func main() {
fmt.Println("Begin doing something!")
c := make(chan bool)
go func() {
fmt.Println("Doing something…")
close(c)
}()
<-c
fmt.Println("Done!")
}
這裡main goroutine通過"<-c"來等待sub goroutine中的“完成事件”,sub goroutine通過close channel促發這一事件。當然也可以通過向Channel寫入一個bool值的方式來作為事件通知。main goroutine在channel c上沒有任何資料可讀的情況下會阻塞等待。
2、協同多個Goroutines
同上,close channel還可以用於協同多個Goroutines,比如下面這個例子,我們建立了100個Worker Goroutine,這些Goroutine在被建立出來後都阻塞在"<-start"上,直到我們在main goroutine中給出開工的訊號:"close(start)",這些goroutines才開始真正的並發運行起來。
//testwaitevent2.go
package main
import "fmt"
func worker(start chan bool, index int) {
<-start
fmt.Println("This is Worker:", index)
}
func main() {
start := make(chan bool)
for i := 1; i <= 100; i++ {
go worker(start, i)
}
close(start)
select {} //deadlock we expected
}
3、Select
從不同的並發執行的協程中擷取值可以通過關鍵字select
來完成,它和switch
控制語句非常相似(章節5.3)也被稱作通訊開關;它的行為像是“你準備好了嗎”的輪詢機制;select
監聽進入通道的資料,也可以是用通道發送值的時候。
select
做的就是:選擇處理列出的多個通訊情況中的一個。
- 如果都阻塞了,會等待直到其中一個可以處理
- 如果多個可以處理,隨機播放一個
- 如果沒有通道操作可以處理並且寫了
default
語句,它就會執行:default
永遠是可啟動並執行(這就是準備好了,可以執行)。
下面是select的基本操作。
select {
case x := <- somechan:
// … 使用x進行一些操作
case y, ok := <- someOtherchan:
// … 使用y進行一些操作,
// 檢查ok值判斷someOtherchan是否已經關閉
case outputChan <- z:
// … z值被成功發送到Channel上時
default:
// … 上面case均無法通訊時,執行此分支
}
我想這裡John Graham-Cumming主要是想告訴我們select的default分支的實踐用法。
1、select for non-blocking receive
idle:= make(chan []byte, 5) //用一個帶緩衝的channel構造一個簡單的隊列
select {
case b = <-idle:
//嘗試從idle隊列中讀取
…
default: //隊列空,分配一個新的buffer
makes += 1
b = make([]byte, size)
}
2、select for non-blocking send
idle:= make(chan []byte, 5) //用一個帶緩衝的channel構造一個簡單的隊列
select {
case idle <- b: //嘗試向隊列中插入一個buffer
//…
default: //隊列滿?
}
【慣用法:for/select】
我們在使用select時很少只是對其進行一次evaluation,我們常常將其與for {}結合在一起使用,並選擇適當時機從for{}中退出。
for {
select {
case x := <- somechan:
// … 使用x進行一些操作
case y, ok := <- someOtherchan:
// … 使用y進行一些操作,
// 檢查ok值判斷someOtherchan是否已經關閉
case outputChan <- z:
// … z值被成功發送到Channel上時
default:
// … 上面case均無法通訊時,執行此分支
}
}
【終結workers】
下面是一個常見的終結sub worker goroutines的方法,每個worker goroutine通過select監視一個die channel來及時擷取main goroutine的退出通知。
//testterminateworker1.go
package main
import (
"fmt"
"time"
)
func worker(die chan bool, index int) {
fmt.Println("Begin: This is Worker:", index)
for {
select {
//case xx:
//做事的分支
case <-die:
fmt.Println("Done: This is Worker:", index)
return
}
}
}
func main() {
die := make(chan bool)
for i := 1; i <= 100; i++ {
go worker(die, i)
}
time.Sleep(time.Second * 5)
close(die)
select {} //deadlock we expected
}
【終結驗證】
有時候終結一個worker後,main goroutine想確認worker routine是否真正退出了,可採用下面這種方法:
//testterminateworker2.go
package main
import (
"fmt"
//"time"
)
func worker(die chan bool) {
fmt.Println("Begin: This is Worker")
for {
select {
//case xx:
//做事的分支
case <-die:
fmt.Println("Done: This is Worker")
die <- true
return
}
}
}
func main() {
die := make(chan bool)
go worker(die)
die <- true
<-die
fmt.Println("Worker goroutine has been terminated")
}
【關閉的Channel永遠不會阻塞】
通道可以被顯式的關閉;儘管它們和檔案不同:不必每次都關閉。只有在當需要告訴接收者不會再提供新的值的時候,才需要關閉通道。只有寄件者需要關閉通道,接收者永遠不會需要。
下面示範在一個已經關閉了的channel上讀寫的結果:
//testoperateonclosedchannel.go
package main
import "fmt"
func main() {
cb := make(chan bool)
close(cb)
x := <-cb
fmt.Printf("%#v\n", x)
x, ok := <-cb
fmt.Printf("%#v %#v\n", x, ok)
ci := make(chan int)
close(ci)
y := <-ci
fmt.Printf("%#v\n", y)
cb <- true
}
$go run testoperateonclosedchannel.go
false
false false
0
panic: runtime error: send on closed channel
可以看到在一個已經close的unbuffered channel上執行讀操作,回返回channel對應類型的零值,比如bool型channel返回false,int型channel返回0。但向close的channel寫則會觸發panic。不過無論讀寫都不會導致阻塞。
【關閉帶緩衝的channel】
將unbuffered channel換成buffered channel會怎樣?我們看下面例子:
//testclosedbufferedchannel.go
package main
import "fmt"
func main() {
c := make(chan int, 3)
c <- 15
c <- 34
c <- 65
close(c)
fmt.Printf("%d\n", <-c)
fmt.Printf("%d\n", <-c)
fmt.Printf("%d\n", <-c)
fmt.Printf("%d\n", <-c)
c <- 1
}
$go run testclosedbufferedchannel.go
15
34
65
0
panic: runtime error: send on closed channel
可以看出帶緩衝的channel略有不同。儘管已經close了,但我們依舊可以從中讀出關閉前寫入的3個值。第四次讀取時,則會返回該channel類型的零值。向這類channel寫入操作也會觸發panic。
四、隱藏狀態(自增長ID產生器)
下面通過一個例子來示範一下channel如何用來隱藏狀態:
1、例子:唯一的ID服務
//testuniqueid.go
package main
import "fmt"
func newUniqueIDService() <-chan string {
id := make(chan string)
go func() {
var counter int64 = 0
for {
id <- fmt.Sprintf("%x", counter)
counter += 1
}
}()
return id
}
func main() {
id := newUniqueIDService()
for i := 0; i < 10; i++ {
fmt.Println(<-id)
}
}
newUniqueIDService通過一個channel與main goroutine關聯,main goroutine無需知道uniqueid實現的細節以及目前狀態,只需通過channel獲得最新id即可。
五、預設情況(最常見的方式:生產者/消費者)
生產者產生一些資料將其放入 channel;然後消費者按照順序,一個一個的從 channel 中取出這些資料進行處理。這是最常見的 channel 的使用方式。當 channel 的緩衝用盡時,生產者必須等待(阻塞)。換句話說,若是 channel 中沒有資料,消費者就必須等待了。
生產者
func producer(c chan int64, max int) { defer close(c) for i:= 0; i < max; i ++ { c <- time.Now().Unix() }}
生產者產生“max”個 int64 的數字,並且將其放入 channel “c” 中。需要注意的是,這裡用 defer 在函數推出的時候關閉了 channel。
消費者
func consumer(c chan int64) { var v int64 ok := true for ok { if v, ok = <-c; ok { fmt.Println(v) } }}
從 channel 中一個一個的讀取 int64 的數字,然後將其列印在螢幕上。當 channel 被關閉後,變數“ok”將被設定為“false”。
六、Nil Channels
1、nil channels阻塞
對一個沒有初始化的channel進行讀寫操作都將發生阻塞,例子如下:
package main
func main() {
var c chan int
<-c
}
$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!
package main
func main() {
var c chan int
c <- 1
}
$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!
2、nil channel在select中很有用
看下面這個例子:
//testnilchannel_bad.go
package main
import "fmt"
import "time"
func main() {
var c1, c2 chan int = make(chan int), make(chan int)
go func() {
time.Sleep(time.Second * 5)
c1 <- 5
close(c1)
}()
go func() {
time.Sleep(time.Second * 7)
c2 <- 7
close(c2)
}()
for {
select {
case x := <-c1:
fmt.Println(x)
case x := <-c2:
fmt.Println(x)
}
}
fmt.Println("over")
}
我們原本期望程式交替輸出5和7兩個數字,但實際的輸出結果卻是:
5
0
0
0
… … 0死迴圈
再仔細分析代碼,原來select每次按case順序evaluate:
– 前5s,select一直阻塞;
– 第5s,c1返回一個5後被close了,“case x := <-c1”這個分支返回,select輸出5,並重新select
– 下一輪select又從“case x := <-c1”這個分支開始evaluate,由於c1被close,按照前面的知識,close的channel不會阻塞,我們會讀出這個 channel對應類型的零值,這裡就是0;select再次輸出0;這時即便c2有值返回,程式也不會走到c2這個分支
– 依次類推,程式無限迴圈的輸出0
我們利用nil channel來改進這個程式,以實現我們的意圖,代碼如下:
//testnilchannel.go
package main
import "fmt"
import "time"
func main() {
var c1, c2 chan int = make(chan int), make(chan int)
go func() {
time.Sleep(time.Second * 5)
c1 <- 5
close(c1)
}()
go func() {
time.Sleep(time.Second * 7)
c2 <- 7
close(c2)
}()
for {
select {
case x, ok := <-c1:
if !ok {
c1 = nil
} else {
fmt.Println(x)
}
case x, ok := <-c2:
if !ok {
c2 = nil
} else {
fmt.Println(x)
}
}
if c1 == nil && c2 == nil {
break
}
}
fmt.Println("over")
}
$go run testnilchannel.go
5
7
over
可以看出:通過將已經關閉的channel置為nil,下次select將會阻塞在該channel上,使得select繼續下面的分支evaluation。
七、Timers(逾時定時器)
1、逾時機制Timeout
帶逾時機制的select是常規的tip,下面是範例程式碼,實現30s的逾時select:
func worker(start chan bool) {
timeout := time.After(30 * time.Second)
for {
select {
// … do some stuff
case <- timeout:
return
}
}
}
2、心跳HeartBeart
與timeout實作類別似,下面是一個簡單的心跳select實現:
func worker(start chan bool) {
heartbeat := time.Tick(30 * time.Second)
for {
select {
// … do some stuff
case <- heartbeat:
//… do heartbeat stuff
}
}
}
參考自:
http://blog.csdn.net/erlib/article/details/44097291
http://tonybai.com/2014/09/29/a-channel-compendium-for-golang/