Golang channel 用法簡介

channel 是 golang 裏至關有趣的一個功能,大部分時候 channel 都是和 goroutine 一塊兒配合使用。本文主要介紹 channel 的一些有趣的用法。golang

通道(channel),像是通道(管道),能夠經過它們發送類型化的數據在協程之間通訊,能夠避開全部內存共享致使的坑;通道的通訊方式保證了同步性。數據經過通道:同一時間只有一個協程能夠訪問數據:因此不會出現數據競爭,設計如此。數據的歸屬(能夠讀寫數據的能力)被傳遞。shell

通道其實是類型化消息的隊列:使數據得以傳輸。它是先進先出(FIFO)結構的因此能夠保證發送給他們的元素的順序(有些人知道,通道能夠比做 Unix shells 中的雙向管道(tw-way pipe))。通道也是引用類型,因此咱們使用 make() 函數來給它分配內存。緩存

2、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
lua

默認狀況下,通訊是同步且無緩衝的:在有接受者接收數據以前,發送不會結束。能夠想象一個無緩衝的通道在沒有空間來保存數據的時候:必需要一個接收者準備好接收通道的數據而後發送者能夠直接把數據發送給接收者。因此通道的發送/接收操做在對方準備好以前是阻塞的:spa

1)對於同一個通道,發送操做(協程或者函數中的),在接收者準備好以前是阻塞的:若是ch中的數據無人接收,就沒法再給通道傳入其餘數據:新的輸入沒法在通道非空的狀況下傳入。因此發送操做會等待 ch 再次變爲可用狀態:就是通道值被接收時(能夠傳入變量)。.net

2)對於同一個通道,接收操做是阻塞的(協程或函數中的),直到發送者可用:若是通道中沒有數據,接收者就阻塞了。設計

3、Channel用做信號(Signal)的場景(信號量)code

一、等待一個事件(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上沒有任何數據可讀的狀況下會阻塞等待。

二、協同多個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
}

三、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分支的實踐用法。

 

一、select  for non-blocking receive

 

 

 

idle:= make(chan []byte, 5) //用一個帶緩衝的channel構造一個簡單的隊列

 

select {
case b = <-idle:
 //嘗試從idle隊列中讀取
    …
default:  //隊列空,分配一個新的buffer
        makes += 1
        b = make([]byte, size)
}

 

二、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。

 

4、隱藏狀態(自增加ID生成器)

下面經過一個例子來演示一下channel如何用來隱藏狀態:

一、例子:惟一的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便可。

5、默認狀況(最多見的方式:生產者/消費者)

生產者產生一些數據將其放入 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」。

6、Nil Channels

一、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!

 

二、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。

 

7、Timers(超時定時器)

 

一、超時機制Timeout

 

帶超時機制的select是常規的tip,下面是示例代碼,實現30s的超時select:

 

func worker(start chan bool) {
        timeout := time.After(30 * time.Second)
        for {
                select {
                     // … do some stuff
                case <- timeout:
                    return
                }
        }
}

 

二、心跳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/

相關文章
相關標籤/搜索