深入學習golang(2)—channel
Channel
1. 概述
“網路,併發”是Go語言的兩大feature。Go語言號稱“網際網路的C語言”,與使用傳統的C語言相比,寫一個Server所使用的程式碼更少,也更簡單。寫一個Server除了網路,另外就是併發,相對python等其它語言,Go對併發支援使得它有更好的效能。
Goroutine和channel是Go在“併發”方面兩個核心feature。
Channel是goroutine之間進行通訊的一種方式,它與Unix中的管道類似。
Channel宣告:
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
例如:
var ch chan int
var ch1 chan<- int //ch1只能寫
var ch2 <-chan int //ch2只能讀
channel是型別相關的,也就是一個channel只能傳遞一種型別。例如,上面的ch只能傳遞int。
在go語言中,有4種引用型別:slice,map,channel,interface。
Slice,map,channel一般都通過make進行初始化:
ci := make(chan int) // unbuffered channel of integers
cj := make(chan int, 0) // unbuffered channel of integers
cs := make(chan *os.File, 100) // buffered channel of pointers to Files
建立channel時可以提供一個可選的整型引數,用於設定該channel的緩衝區大小。該值預設為0,用來構建預設的“無緩衝channel”,也稱為“同步channel”。
Channel作為goroutine間的一種通訊機制,與作業系統的其它通訊機制類似,一般有兩個目的:同步,或者傳遞訊息。
2. 同步
c := make(chan int) // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
list.Sort()
c <- 1 // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c // Wait for sort to finish; discard sent value.
上面的示例中,在子goroutine中進行排序操作,主goroutine可以做一些別的事情,然後等待子goroutine完成排序。
接收方會一直阻塞直到有資料到來。如果channel是無緩衝的,傳送方會一直阻塞直到接收方將資料取出。如果channel帶有緩衝區,傳送方會一直阻塞直到資料被拷貝到緩衝區;如果緩衝區已滿,則傳送方只能在接收方取走資料後才能從阻塞狀態恢復。
3. 訊息傳遞
我們來模擬一下經典的生產者-消費者模型。
func Producer (queue chan<- int){
for i:= 0; i < 10; i++ {
queue <- i
}
}
func Consumer( queue <-chan int){
for i :=0; i < 10; i++{
v := <- queue
fmt.Println("receive:", v)
}
}
func main(){
queue := make(chan int, 1)
go Producer(queue)
go Consumer(queue)
time.Sleep(1e9) //讓Producer與Consumer完成
}
上面的示例在Producer中生成資料,在Consumer中處理資料。
4. Server程式設計模型
在server程式設計,一種常用的模型:主執行緒接收請求,然後將請求分發給工作執行緒,工作執行緒完成請求處理。用go來實現,如下:
func handle(r *Request) {
process(r) // May take a long time.
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Don't wait for handle to finish.
}
}
一般來說,server的處理能力不是無限的,所以,有必要限制執行緒(或者goroutine)的數量。在C/C++程式設計中,我們一般通過訊號量來實現,在go中,我們可以通過channel達到同樣的效果:
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // Wait for active queue to drain.
process(r) // May take a long time.
<-sem // Done; enable next request to run.
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Don't wait for handle to finish.
}
}
我們通過引入sem channel,限制了同時最多隻有MaxOutstanding個goroutine執行。但是,上面的做法,只是限制了執行的goroutine的數量,並沒有限制goroutine的生成數量。如果請求到來的速度過快,會導致產生大量的goroutine,這會導致系統資源消耗完全。
為此,我們有必要限制goroutine的建立數量:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // Buggy; see explanation below.
<-sem
}()
}
}
上面的程式碼看似簡單清晰,但在go中,卻有一個問題。Go語言中的迴圈變數每次迭代中是重用的,更直接的說就是req在所有的子goroutine中是共享的,從變數的作用域角度來說,變數req對於所有的goroutine,是全域性的。
這個問題屬於語言實現的範疇,在C語言中,你不應該將一個區域性變數傳遞給另外一個執行緒去處理。有很多解決方法,這裡有一個討論。從個人角度來說,我更傾向下面這種方式:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(r *Request) {
process(r)
<-sem
}(req)
}
}
至少,這樣的程式碼不會讓一個go的初學者不會迷糊,另外,從變數的作用域角度,也更符合常理一些。
在實際的C/C++程式設計中,我們傾向於工作執行緒在一開始就建立好,而且執行緒的數量也是固定的。在go中,我們也可以這樣做:
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Start handlers
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Wait to be told to exit.
}
開始就啟動固定數量的handle goroutine,每個goroutine都直接從channel中讀取請求。這種寫法比較簡單,但是不知道有沒有“驚群”問題?有待後續分析goroutine的實現。
5. 傳遞channel的channel
channel作為go語言的一種原生型別,自然可以通過channel進行傳遞。通過channel傳遞channel,可以非常簡單優美的解決一些實際中的問題。
在上一節中,我們主goroutine通過channel將請求傳遞給工作goroutine。同樣,我們也可以通過channel將處理結果返回給主goroutine。
主goroutine:
type Request struct {
args []int
resultChan chan int
}
request := &Request{[]int{3, 4, 5}, make(chan int)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)
主goroutine將請求發給request channel,然後等待result channel。子goroutine完成處理後,將結果寫到result channel。
func handle(queue chan *Request) {
for req := range queue {
result := do_something()
req.resultChan <- result
}
}
6. 多個channel
在實際程式設計中,經常會遇到在一個goroutine中處理多個channel的情況。我們不可能阻塞在兩個channel,這時就該select場了。與C語言中的select可以監控多個fd一樣,go語言中select可以等待多個channel。
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(time.Second * 1)
c1 <- "one"
}()
go func() {
time.Sleep(time.Second * 2)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
在C中,我們一般都會傳一個超時時間給select函式,go語言中的select沒有該引數,相當於超時時間為0。
主要參考
https://golang.org/doc/effective_go.html
作者:YY哥
出處:http://www.cnblogs.com/hustcat/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。