深入學習golang(2)—channel

來源:互聯網
上載者:User

 

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.