1、Golang併發基礎理論程序員
Golang在併發設計方面參考了C.A.R Hoare的CSP,即Communicating Sequential Processes併發模型理論。但就像John Graham-Cumming所說的那樣,多數Golang程序員或愛好者僅僅停留在「知道」這一層次,理解CSP理論的並很少,畢竟多數程序員是搞工程 的。不過要想系統學習CSP的人能夠從這裏下載到CSP論文的最新版本。golang
維基百科中概要羅列了CSP模型與另一種併發模型Actor模型的區別:緩存
Actor模型廣義上講與CSP模型很類似。但兩種模型就提供的原語而言,又有一些根本上的不一樣之處:
– CSP模型處理過程是匿名的,而Actor模型中的Actor則具備身份標識。
– CSP模型的消息傳遞在收發消息進程間包含了一個交會點,即發送方只能在接收方準備好接收消息時才能發送消息。相反,actor模型中的消息傳遞是異步 的,即消息的發送和接收無需在同一時間進行,發送方能夠在接收方準備好接收消息前將消息發送出去。這兩種方案能夠認爲是彼此對偶的。在某種意義下,基於交 會點的系統能夠經過構造帶緩衝的通訊的方式來模擬異步消息系統。而異步系統能夠經過構造帶消息/應答協議的方式來同步發送方和接收方來模擬交會點似的通訊 方式。
– CSP使用顯式的Channel用於消息傳遞,而Actor模型則將消息發送給命名的目的Actor。這兩種方法能夠被認爲是對偶的。某種意義下,進程可 以從一個實際上擁有身份標識的channel接收消息,而經過將actors構形成類Channel的行爲模式也能夠打破actors之間的名字耦合。併發
2、Go Channel基本操做語法app
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兼具通訊和同步兩種特性,頗受青睞。lua
3、Channel用做信號(Signal)的場景spa
一、等待一個事件(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中讀取全部值。下面是一個簡單的實例:
//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
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 } } }