Golang Channel用法簡編

在進入正式內容前,我這裏先順便轉發一則消息,那就是Golang 1.3.2已經正式發佈了。國內的golangtc已經鏡像了golang.org的安裝包下載頁面,國內go程序員與愛好者們能夠到"Golang中 國",即golangtc.com去下載go 1.3.2版本。 git

Go這門語言也許你還不甚瞭解,甚至是徹底不知道,這也有情可原,畢竟Go在TIOBE編程語言排行榜上位列30開外。但近期使用Golang 實現的一殺手級應用 Docker你卻不應不知道。docker目前火得是一塌糊塗啊。你去國內外各大技術站點用眼輕瞥一下,如 果沒有涉及到「docker」字樣新聞的站點建 議你之後就不要再去訪問了^_^。Docker是啥、怎麼用以及基礎實踐能夠參加國內一位仁兄的經驗之做:《 Docker – 從入門到實踐》。 程序員

據我瞭解,目前國內試水Go語言開發後臺系統的大公司與初創公司日益增多,好比七牛、京東、小米,盛大,金山,東軟,搜狗等,在這裏咱們能夠看到一些公司的Go語言應用列表,而且目前這個列表彷佛依舊在豐富中。國內Go語言的推廣與佈道也再穩步推動中,不過目前來看多以Go入 門與基礎爲主題,Go idioms、tips或Best Practice的Share並很少見,想必國內的先行者、佈道師們還在韜光養晦,積攢經驗,等到時機來臨再厚積薄發。另外國內彷佛尚未一個針對Go的 佈道平臺,好比Golang技術大會之類的的平臺。 github

在國外,雖然Go也剛剛起步,但在Golang share的廣度和深度方面顯然更進一步。Go的國際會議目前還很少,除了Golang老東家Google在本身的各類大會上留給Golang展現本身的 機會外,由 Gopher Academy 發起的GopherCon 會議也於今年第一次舉行,並放出諸多高質量資料,在這裏能夠下載。歐洲的Go語言大會.dotgo也即將開幕,估計後續這兩個大會將撐起Golang技術分享 的旗幟。 golang

言歸正傳,這裏要寫的東西並不是原創,本身的Go僅僅算是入門級別,工程經驗、Best Practice等還談不上有多少,所以這裏主要是針對GopherCon2014上的「舶來品」的學習心得。來自CloudFlare的工程師John Graham-Cumming談了關於 Channel的實踐經驗,這裏針對其分享的內容,記錄一些學習體會和理解,並結合一些外延知識,也能夠算是一種學習筆記吧,僅供參考。 docker

1、Golang併發基礎理論 編程

Golang在併發設計方面參考了C.A.R Hoare的CSP,即Communicating Sequential Processes併發模型理論。但就像John Graham-Cumming所說的那樣,多數Golang程序員或愛好者僅僅停留在「知道」這一層次,理解CSP理論的並很少,畢竟多數程序員是搞工程 的。不過要想系統學習CSP的人能夠從這裏下載到CSP論文的最新版本。 緩存

維基百科中概要羅列了CSP模型與另一種併發模型Actor模型的區別: 併發

Actor模型廣義上講與CSP模型很類似。但兩種模型就提供的原語而言,又有一些根本上的不一樣之處:
    – CSP模型處理過程是匿名的,而Actor模型中的Actor則具備身份標識。
    – CSP模型的消息傳遞在收發消息進程間包含了一個交會點,即發送方只能在接收方準備好接收消息時才能發送消息。相反,actor模型中的消息傳遞是異步 的,即消息的發送和接收無需在同一時間進行,發送方能夠在接收方準備好接收消息前將消息發送出去。這兩種方案能夠認爲是彼此對偶的。在某種意義下,基於交 會點的系統能夠經過構造帶緩衝的通訊的方式來模擬異步消息系統。而異步系統能夠經過構造帶消息/應答協議的方式來同步發送方和接收方來模擬交會點似的通訊 方式。
    – CSP使用顯式的Channel用於消息傳遞,而Actor模型則將消息發送給命名的目的Actor。這兩種方法能夠被認爲是對偶的。某種意義下,進程可 以從一個實際上擁有身份標識的channel接收消息,而經過將actors構形成類Channel的行爲模式也能夠打破actors之間的名字耦合。 app

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

不帶緩衝的Channel兼具通訊和同步兩種特性,頗受青睞。

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

一、等待一個事件(Event)

等待一個事件,有時候經過close一個Channel就足夠了。例如:

//testwaitevent1.go
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上沒有任何數據可讀的狀況下會阻塞等待。

關於輸出結果:

根據《Go memory model》中關於close channel與recv from channel的order的定義:The closing of a channel happens before a receive that returns a zero value because the channel is closed.

咱們能夠很容易判斷出上面程序的輸出結果:

Begin doing something!
Doing something…
Done!

若是將close(c)換成c<-true,則根據《Go memory model》中的定義:A receive from an unbuffered channel happens before the send on that channel completes.
"<-c"要先於"c<-true"完成,但也不影響日誌的輸出順序,輸出結果仍爲上面三行。

二、協同多個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的基本操做】
select是Go語言特有的操做,使用select咱們能夠同時在多個channel上進行發送/接收操做。下面是select的基本操做。

select {
case x := <- somechan:
    // … 使用x進行一些操做

case y, ok := <- someOtherchan:
    // … 使用y進行一些操做,
    // 
檢查ok值判斷someOtherchan是否已經關閉

case outputChan <- z:
    // … z值被成功發送到Channel上時

default:
    // … 上面case均沒法通訊時,執行此分支
}

【慣用法: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。

【range】

Golang中的range經常和channel並肩做戰,它被用來從channel中讀取全部值。下面是一個簡單的實例:

//testrange.go
package main

import "fmt"

func generator(strings chan string) {
        strings <- "Five hour's New York jet lag"
        strings <- "and Cayce Pollard wakes in Camden Town"
        strings <- "to the dire and ever-decreasing circles"
        strings <- "of disrupted circadian rhythm."
        close(strings)
}

func main() {
        strings := make(chan string)
        go generator(strings)
        for s := range strings {
                fmt.Printf("%s\n", s)
        }
        fmt.Printf("\n")
}

4、隱藏狀態

下面經過一個例子來演示一下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)
        }
}

$ go run testuniqueid.go
0
1
2
3
4
5
6
7
8
9

newUniqueIDService經過一個channel與main goroutine關聯,main goroutine無需知道uniqueid實現的細節以及當前狀態,只需經過channel得到最新id便可。

5、默認狀況

我想這裏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: //隊列滿?

}

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                 }         } }

相關文章
相關標籤/搜索