Go基礎系列:channel入門

原文作者:駿馬金龍 來源:博客園

channel基礎

channel用於goroutines之間的通信,讓它們之間可以進行數據交換。像管道一樣,一個goroutine_A向channel_A中放數據,另一個goroutine_B從channel_A取數據

channel是指針類型的數據類型,通過make來分配內存。例如:

1ch := make(chan int)

這表示創建一個channel,這個channel中只能保存int類型的數據。也就是說一端只能向此channel中放進int類型的值,另一端只能從此channel中讀出int類型的值。

需要注意,chan TYPE才表示channel的類型。所以其作爲參數或返回值時,需指定爲xxx chan int類似的格式。

向ch這個channel放數據的操作形式爲:

1ch <- VALUE

從ch這個channel讀數據的操作形式爲:

1<-ch             // 從ch中讀取一個值val = <-chval := <-ch      // 從ch中讀取一個值並保存到val變量中val,ok = <-ch    // 從ch讀取一個值,判斷是否讀取成功,如果成功則保存到val變量中

其實很簡單,當ch出現在<-的左邊表示send,當ch出現在<-的右邊表示recv。

例如:

 1package mainimport (    "fmt"
 2    "time")func main() {
 3    ch := make(chan string)    go sender(ch)         // sender goroutine
 4    go recver(ch)         // recver goroutine
 5    time.Sleep(1e9)
 6}func sender(ch chan string) {
 7    ch <- "malongshuai"
 8    ch <- "gaoxiaofang"
 9    ch <- "wugui"
10    ch <- "tuner"}func recver(ch chan string) {    var recv string
11    for {
12        recv = <-ch
13        fmt.Println(recv)
14    }
15}

輸出結果:

1malongshuaigaoxiaofang
2wugui
3tuner

上面激活了一個goroutine用於執行sender()函數,該函數每次向channel ch中發送一個字符串。同時還激活了另一個goroutine用於執行recver()函數,該函數每次從channel ch中讀取一個字符串。

注意上面的recv = <-ch,當channel中沒有數據可讀時,recver goroutine將會阻塞在此行。由於recver中讀取channel的操作放在了無限for循環中,表示recver goroutine將一直阻塞,直到從channel ch中讀取到數據,讀取到數據後進入下一輪循環由被阻塞在recv = <-ch上。直到main中的time.Sleep()指定的時間到了,main程序終止,所有的goroutine將全部被強制終止。

因爲receiver要不斷從channel中讀取可能存在的數據,所以receiver一般都使用一個無限循環來讀取channel,避免sender發送的數據被丟棄。

channel的屬性和分類

每個channel都有3種操作:send、receive和close

  • send:表示sender端的goroutine向channel中投放數據
  • receive:表示receiver端的goroutine從channel中讀取數據
  • close:表示關閉channel
    • 關閉channel後,send操作將導致painc
    • 關閉channel後,recv操作將返回對應類型的0值以及一個狀態碼false
    • close並非強制需要使用close(ch)來關閉channel,在某些時候可以自動被關閉
    • 如果使用close(),建議條件允許的情況下加上defer
    • 只在sender端上顯式使用close()關閉channel。因爲關閉通道意味着沒有數據再需要發送

例如,判斷channel是否被關閉:

1val, ok := <-counterif ok {
2    fmt.Println(val)
3}

channel分爲兩種:unbuffered channel和buffered channel

  • unbuffered channel:阻塞、同步模式
    • sender端向channel中send一個數據,然後阻塞,直到receiver端將此數據receive
    • receiver端一直阻塞,直到sender端向channel發送了一個數據
  • buffered channel:非阻塞、異步模式
    • sender端可以向channel中send多個數據(只要channel容量未滿),容量滿之前不會阻塞
    • receiver端按照隊列的方式(FIFO,先進先出)從buffered channel中按序receive其中數據

buffered channel有兩個屬性:容量和長度:和slice的capacity和length的概念是一樣的

  • capacity:表示bufffered channel最多可以緩衝多少個數據
  • length:表示buffered channel當前已緩衝多少個數據
  • 創建buffered channel的方式爲make(chan TYPE,CAP)

unbuffered channel可以認爲是容量爲0的buffered channel,所以每發送一個數據就被阻塞。注意,不是容量爲1的buffered channel,因爲容量爲1的channel,是在channel中已有一個數據,併發送第二個數據的時候才被阻塞。

換句話說,send被阻塞的時候,其實是沒有發送成功的,只有被另一端讀走一個數據之後纔算是send成功。對於unbuffered channel來說,這是send/recv的同步模式。

實際上,當向一個channel進行send的時候,先關閉了channel,再讀取channel時會發現錯誤在send,而不是recv。它會提示向已經關閉了的channel發送數據。

1func main() {
2    counter := make(chan int)    go func() {
3        counter <- 32
4    }()    close(counter)
5    fmt.Println(<-counter)
6}

輸出報錯:

1panic: send on closed channel

所以,在Go的內部行爲中,send和recv是一個整體行爲,數據未讀就表示未send成功

死鎖(deadlock)

當channel的某一端(sender/receiver)期待另一端的(receiver/sender)操作,另一端正好在期待本端的操作時,也就是說兩端都因爲對方而使得自己當前處於阻塞狀態,這時將會出現死鎖問題。

比如,在main函數中,它有一個默認的goroutine,如果在此goroutine中創建一個unbuffered channel,並在main goroutine中向此channel中發送數據並直接receive數據,將會出現死鎖:

1package main 
2
3import (    "fmt")func main (){
4    goo(32)
5}func goo(s int) {
6    counter := make(chan int)
7    counter <- s
8    fmt.Println(<-counter)
9}

在上面的示例中,向unbuffered channel中send數據的操作counter <- s是在main goroutine中進行的,從此channel中recv的操作<-counter也是在main goroutine中進行的。send的時候會直接阻塞main goroutine,使得recv操作無法被執行,go將探測到此問題,並報錯:

1fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:

要修復此問題,只需將send操作放在另一個goroutine中執行即可:

1package mainimport (    "fmt")func main() {
2    goo(32)
3}func goo(s int) {
4    counter := make(chan int)    go func() {
5        counter <- s
6    }()
7    fmt.Println(<-counter)
8}

或者,將counter設置爲一個容量爲1的buffered channel:

1counter := make(chan int,1)

這樣放完一個數據後send不會阻塞(被recv之前放第二個數據纔會阻塞),可以執行到recv操作。

unbuffered channel同步通信示例

下面通過sync.WaitGroup類型來等待程序的結束,分析多個goroutine之間通信時狀態的轉換。因爲創建的channel是unbuffered類型的,所以send和recv都是阻塞的。

 1package mainimport (    "fmt"
 2    "sync")// wg用於等待程序執行完成var wg sync.WaitGroupfunc main() {
 3    count := make(chan int)    // 增加兩個待等待的goroutines
 4    wg.Add(2)
 5    fmt.Println("Start Goroutines")    // 激活一個goroutine,label:"Goroutine-1"
 6    go printCounts("Goroutine-1", count)    // 激活另一個goroutine,label:"Goroutine-2"
 7    go printCounts("Goroutine-2", count)
 8
 9    fmt.Println("Communication of channel begins")    // 向channel中發送初始數據
10    count <- 1
11
12    // 等待goroutines都執行完成
13    fmt.Println("Waiting To Finish")
14    wg.Wait()
15    fmt.Println("\nTerminating the Program")
16}func printCounts(label string, count chan int) {    // goroutine執行完成時,wg的計數器減1
17    defer wg.Done()    for {        // 從channel中接收數據
18        // 如果無數據可recv,則goroutine阻塞在此
19        val, ok := <-count        if !ok {
20            fmt.Println("Channel was closed:",label)            return
21        }
22        fmt.Printf("Count: %d received from %s \n", val, label)        if val == 10 {
23            fmt.Printf("Channel Closed from %s \n", label)            // Close the channel
24            close(count)            return
25        }        // 輸出接收到的數據後,加1,並重新將其send到channel中
26        val++
27        count <- val
28    }
29}

上面的程序中,激活了兩個goroutine,激活這兩個goroutine後,向channel中發送一個初始數據值1,然後main goroutine將因爲wg.Wait()等待2個goroutine都執行完成而被阻塞。

再看這兩個goroutine,這兩個goroutine執行完全一樣的函數代碼,它們都接收count這個channel的數據,但可能是goroutine1先接收到channel中的初始值1,也可能是goroutine2先接收到初始值1。接收到數據後輸出值,並在輸出後對數據加1,然後將加1後的數據再次send到channel,每次send都會將自己這個goroutine阻塞(因爲unbuffered channel),此時另一個goroutine因爲等待recv而執行。當加1後發送給channel的數據爲10之後,某goroutine將關閉count channel,該goroutine將退出,wg的計數器減1,另一個goroutine因等待recv而阻塞的狀態將因爲channel的關閉而失敗,ok狀態碼將讓該goroutine退出,於是wg的計數器減爲0,main goroutine因爲wg.Wait()而繼續執行後面的代碼。

使用for range迭代channel

前面都是在for無限循環中讀取channel中的數據,但也可以使用range來迭代channel,它會返回每次迭代過程中所讀取的數據,直到channel被關閉。

例如,將上面示例中的printCounts()改爲for-range的循環形式。

1func printCounts(label string, count chan int) {    defer wg.Done()    for val := range count {
2        fmt.Printf("Count: %d received from %s \n", val, label)        if val == 10 {
3            fmt.Printf("Channel Closed from %s \n", label)            close(count)            return
4        }
5        val++
6        count <- val
7    }
8}

多個"管道":輸出作爲輸入

channel是goroutine與goroutine之間通信的基礎,一邊產生數據放進channel,另一邊從channel讀取放進來的數據。可以藉此實現多個goroutine之間的數據交換,例如goroutine_1->goroutine_2->goroutine_3,就像bash的管道一樣,上一個命令的輸出可以不斷傳遞給下一個命令的輸入,只不過golang藉助channel可以在多個goroutine(如函數的執行)之間傳,而bash是在命令之間傳。

以下是一個示例,第一個函數getRandNum()用於生成隨機整數,並將生成的整數放進第一個channel ch1中,第二個函數addRandNum()用於接收ch1中的數據(來自第一個函數),將其輸出,然後對接收的值加1後放進第二個channel ch2中,第三個函數printRes接收ch2中的數據並將其輸出。

如果將函數認爲是Linux的命令,則類似於下面的命令行:ch1相當於第一個管道,ch2相當於第二個管道

1getRandNum | addRandNum | printRes

以下是代碼部分:

 1package mainimport (    "fmt"
 2    "math/rand"
 3    "sync")var wg sync.WaitGroupfunc main() {
 4    wg.Add(3)    // 創建兩個channel
 5    ch1 := make(chan int)
 6    ch2 := make(chan int)    // 3個goroutine並行
 7    go getRandNum(ch1)    go addRandNum(ch1, ch2)    go printRes(ch2)
 8
 9    wg.Wait()
10}func getRandNum(out chan int) {    // defer the wg.Done()
11    defer wg.Done()    var random int
12    // 總共生成10個隨機數
13    for i := 0; i < 10; i++ {        // 生成[0,30)之間的隨機整數並放進channel out
14        random = rand.Intn(30)
15        out <- random
16    }    close(out)
17}func addRandNum(in,out chan int) {    defer wg.Done()    for v := range in {        // 輸出從第一個channel中讀取到的數據
18        // 並將值+1後放進第二個channel中
19        fmt.Println("before +1:",v)
20        out <- (v + 1)
21    }    close(out)
22}func printRes(in chan int){    defer wg.Done()    for v := range in {
23        fmt.Println("after +1:",v)
24    }
25}

指定channel的方向

上面通過兩個channel將3個goroutine連接起來,其中起連接作用的是第二個函數addRandNum()。在這個函數中使用了兩個channel作爲參數:一個channel用於接收、一個channel用於發送。

其實channel類的參數變量可以指定數據流向:

  • in <-chan int:表示channel in通道只用於接收數據
  • out chan<- int:表示channel out通道只用於發送數據

只用於接收數據的通道<-chan不可被關閉,因爲關閉通道是針對發送數據而言的,表示無數據再需發送。對於recv來說,關閉通道是沒有意義的。

所以,上面示例中三個函數可改寫爲:

1func getRandNum(out chan<- int) {
2    ...
3}func addRandNum(in <-chan int, out chan<- int) {
4    ...
5}func printRes(in <-chan int){
6    ...
7}

buffered channel異步隊列請求示例

下面是使用buffered channel實現異步處理請求的示例。

在此示例中:

  • 有(最多)3個worker,每個worker是一個goroutine,它們有worker ID。
  • 每個worker都從一個buffered channel中取出待執行的任務,每個任務是一個struct結構,包含了任務id(JobID),當前任務的隊列號(ID)以及任務的狀態(worker是否執行完成該任務)。
  • 在main goroutine中將每個任務struct發送到buffered channel中,這個buffered channel的容量爲10,也就是最多隻允許10個任務進行排隊。
  • worker每次取出任務後,輸出任務號,然後執行任務(run),最後輸出任務id已完成。
  • 每個worker執行任務的方式很簡單:隨機睡眠0-1秒鐘,並將任務標記爲完成。

以下是代碼部分:

 1package mainimport (    "fmt"
 2    "math/rand"
 3    "sync"
 4    "time")type Task struct {
 5    ID         int
 6    JobID      int
 7    Status     string
 8    CreateTime time.Time
 9}func (t *Task) run() {
10    sleep := rand.Intn(1000)
11    time.Sleep(time.Duration(sleep) * time.Millisecond)
12    t.Status = "Completed"}var wg sync.WaitGroup// worker的數量,即使用多少goroutine執行任務const workerNum = 3func main() {
13    wg.Add(workerNum)    // 創建容量爲10的buffered channel
14    taskQueue := make(chan *Task, 10)    // 激活goroutine,執行任務
15    for workID := 0; workID <= workerNum; workID++ {        go worker(taskQueue, workID)
16    }    // 將待執行任務放進buffered channel,共15個任務
17    for i := 1; i <= 15; i++ {
18        taskQueue <- &Task{
19            ID:         i,
20            JobID:      100 + i,
21            CreateTime: time.Now(),
22        }
23    }    close(taskQueue)
24    wg.Wait()
25}// 從buffered channel中讀取任務,並執行任務func worker(in <-chan *Task, workID int) {    defer wg.Done()    for v := range in {
26        fmt.Printf("Worker%d: recv a request: TaskID:%d, JobID:%d\n", workID, v.ID, v.JobID)
27        v.run()
28        fmt.Printf("Worker%d: Completed for TaskID:%d, JobID:%d\n", workID, v.ID, v.JobID)
29    }
30}

select多路監聽

很多時候想要同時操作多個channel,比如從ch1、ch2讀數據。Go提供了一個select語句塊,它像switch一樣工作,裏面放一些case語句塊,用來輪詢每個case語句塊的send或recv情況。

select

用法格式示例:

1select {    // ch1有數據時,讀取到v1變量中
2    case v1 := <-ch1:
3        ...    // ch2有數據時,讀取到v2變量中
4    case v2 := <-ch2:
5        ...    // 所有case都不滿足條件時,執行default
6    default:
7        ...
8}

defalut語句是可選的,不允許fall through行爲,但允許case語句塊爲空塊。select會被return、break關鍵字中斷。

select的行爲模式主要是對channel是否可讀進行輪詢,但也可以用來向channel發送數據。它的行爲如下:

  • 如果所有的case語句塊都被阻塞,則阻塞直到某個語句塊可以被處理
  • 如果多個case同時滿足條件,則隨機選擇一個進行處理
  • 如果存在default且其它case都不滿足條件,則執行default。所以default必須要可執行而不能阻塞

需要注意的是,如果在select中執行send操作,則可能會永遠被send阻塞。所以,在使用send的時候,應該也使用defalut語句塊,保證send不會被阻塞

一般來說,select會放在一個無限循環語句中,一直輪詢channel的可讀事件。

下面是一個示例,pump1()和pump2()都用於產生數據(一個產生偶數,一個產生奇數),並將數據分別放進ch1和ch2兩個通道,suck()則從ch1和ch2中讀取數據。然後在無限循環中使用select輪詢這兩個通道是否可讀,最後main goroutine在1秒後強制中斷所有goroutine。

 1package mainimport (    "fmt"
 2    "time")func main() {
 3    ch1 := make(chan int)
 4    ch2 := make(chan int)    go pump1(ch1)    go pump2(ch2)    go suck(ch1, ch2)
 5    time.Sleep(1e9)
 6}func pump1(ch chan int) {    for i := 0; i <= 30; i++ {        if i%2 == 0 {
 7            ch <- i
 8        }
 9    }
10}func pump2(ch chan int) {    for i := 0; i <= 30; i++ {        if i%2 == 1 {
11            ch <- i
12        }
13    }
14}func suck(ch1 chan int, ch2 chan int) {    for {        select {        case v := <-ch1:
15            fmt.Printf("Recv on ch1: %d\n", v)        case v := <-ch2:
16            fmt.Printf("Recv on ch2: %d\n", v)
17        }
18    }
19}

版權申明:內容來源網絡,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章