編程學習網 > 編程語言 > go > 學習 Go 協程:詳解信道/通道
2020
03-26

學習 Go 協程:詳解信道/通道


0. 前言

goroutine 是 Go語言程序的并發執行的基本單元,多個 goroutine 的通信是需要依賴本文的主人公 —— channel channel,中文翻譯有叫通道,也有叫信道的。以下為了方便,我統一稱之為 信道 

信道,就是一個管道,連接多個goroutine程序 ,它是一種隊列式的數據結構,遵循先入先出的規則。

1. 信道的定義與使用

每個信道都只能傳遞一種數據類型的數據,所以在你聲明的時候,你得指定數據類型(string int 等等)

var 信道實例 chan 信道類型

聲明后的信道,其零值是nil,無法直接使用,必須配合make函進行初始化。

信道實例 = make(chan 信道類型)

亦或者,上面兩行可以合并成一句,以下我都使用這樣的方式進行信道的聲明

信道實例 := make(chan 信道類型)

假如我要創建一個可以傳輸int類型的信道,可以這樣子寫。

// 定義信道 pipline := make(chan int)

信道的數據操作,無非就兩種:發送數據與讀取數據

// 往信道中發送數據 pipline<- 200 // 從信道中取出數據,并賦值給mydata mydata := <-pipline

信道用完了,可以對其進行關閉,避免有人一直在等待。

close(pipline)

對一個已關閉的信道再關閉,是會報錯的。所以我們還要學會,如何判斷一個信道是否被關閉?

當從信道中讀取數據時,可以有多個返回值,其中第二個可以表示 信道是否被關閉,如果已經被關閉,ok 為 false,若還沒被關閉,ok 為true。

x, ok := <-pipline

2. 信道的容量與長度

一般創建信道都是使用 make 函數,make 函數接收兩個參數

  • 第一個參數:必填,指定信道類型
  • 第二個參數:選填,不填默認為0,指定信道的容量(可緩存多少數據)

對于信道的容量,很重要,這里要多說幾點:

  • 當容量為0時,說明信道中不能存放數據,在發送數據時,必須要求立馬有人接收,否則會報錯。此時的信道稱之為無緩沖信道
  • 當容量為1時,說明信道只能緩存一個數據,若信道中已有一個數據,此時再往里發送數據,會造成程序阻塞。 利用這點可以利用信道來做鎖。
  • 當容量大于1時,信道中可以存放多個數據,可以用于多個協程之間的通信管道,共享資源。

至此我們知道,信道就是一個容器。

若將它比做一個紙箱子

  • 它可以裝10本書,代表其容量為10
  • 當前只裝了1本書,代表其當前長度為1

信道的容量,可以使用 cap 函數獲取 ,而信道的長度,可以使用 len 長度獲取。

package main import "fmt" func main() {
    pipline := make(chan int10)
    fmt.Printf("信道可緩沖 %d 個數據\n"cap(pipline))
    pipline<- 1     fmt.Printf("信道中當前有 %d 個數據"len(pipline))
}

輸出如下

信道可緩沖 10 個數據
信道中當前有 1 個數據

3. 緩沖信道與無緩沖信道

按照是否可緩沖數據可分為:緩沖信道  無緩沖信道

緩沖信道

允許信道里存儲一個或多個數據,這意味著,設置了緩沖區后,發送端和接收端可以處于異步的狀態。

pipline := make(chan int10)

無緩沖信道

在信道里無法存儲數據,這意味著,接收端必須先于發送端準備好,以確保你發送完數據后,有人立馬接收數據,否則發送端就會造成阻塞,原因很簡單,信道中無法存儲數據。也就是說發送端和接收端是同步運行的。

pipline := make(chan int) // 或者 pipline := make(chan int0)

4. 雙向信道與單向信道

通常情況下,我們定義的信道都是雙向通道,可發送數據,也可以接收數據。

但有時候,我們希望對信道的數據流向做一些控制,比如這個信道只能接收數據或者這個信道只能發送數據。

因此,就有了 雙向信道  單向信道 兩種分類。

雙向信道

默認情況下你定義的信道都是雙向的,比如下面代碼

import (
    "fmt"     "time" ) func main() {
    pipline := make(chan int)

    go func() {
        fmt.Println("準備發送數據: 100")
        pipline <- 100     }()

    go func() {
        num := <-pipline
        fmt.Printf("接收到的數據是: %d", num)
    }()
    // 主函數sleep,使得上面兩個goroutine有機會執行     time.Sleep(1)
}

單向信道

單向信道,可以細分為 只讀信道  只寫信道

定義只讀信道

var pipline = make(chan int) type Receiver = <-chan int // 關鍵代碼:定義別名類型 var receiver Receiver = pipline

定義只寫信道

var pipline = make(chan int) type Sender = chan<- int  // 關鍵代碼:定義別名類型 var sender Sender = pipline

仔細觀察,區別在于 <- 符號在關鍵字 chan 的左邊還是右邊。

  • <-chan 表示這個信道,只能從里發出數據,對于程序來說就是只讀
  • chan<- 表示這個信道,只能從外面接收數據,對于程序來說就是只寫

有同學可能會問:為什么還要先聲明一個雙向信道,再定義單向通道呢?比如這樣寫

type Sender = chan<- int 
sender := make(Sender)

代碼是沒問題,但是你要明白信道的意義是什么?(以下是我個人見解

信道本身就是為了傳輸數據而存在的,如果只有接收者或者只有發送者,那信道就變成了只入不出或者只出不入了嗎,沒什么用。所以只讀信道和只寫信道,唇亡齒寒,缺一不可。

當然了,若你往一個只讀信道中寫入數據,或者從一個只寫信道中讀取數據,是必然都會出錯的,不多說了。

完整的示例代碼如下,供你參考:

import (
    "fmt"     "time" )
 //定義只寫信道類型 type Sender = chan<- int   //定義只讀信道類型 type Receiver = <-chan int  func main() {
    var pipline = make(chan int)

    go func() {
        var sender Sender = pipline
        fmt.Println("準備發送數據: 100")
        sender <- 100     }()

    go func() {
        var receiver Receiver = pipline
        num := <-receiver
        fmt.Printf("接收到的數據是: %d", num)
    }()
    // 主函數sleep,使得上面兩個goroutine有機會執行     time.Sleep(1)
}

5. 遍歷信道

遍歷信道,可以使用 for 搭配 range關鍵字,在range時,要確保信道是處于關閉狀態,否則循環會阻塞。

import "fmt" func fibonacci(mychan chan int) {
    n := cap(mychan)
    x, y := 11     for i := 0; i < n; i++ {
        mychan <- x
        x, y = y, x+y
    }
    // 記得 close 信道     // 不然主函數中遍歷完并不會結束,而是會阻塞。     close(mychan)
} func main() {
    pipline := make(chan int10)

    go fibonacci(pipline)

    for k := range pipline {
        fmt.Println(k)
    }
}

6. 用信道來做鎖

當信道里的數據量已經達到設定的容量時,此時再往里發送數據會阻塞整個程序。

利用這個特性,可以用當他來當程序的鎖。

示例如下,詳情可以看注釋

package main import {
    "fmt"     "time" } // 由于 x=x+1 不是原子操作 // 所以應避免多個協程對x進行操作 // 使用容量為1的信道可以達到鎖的效果 func increment(ch chan bool, x *int) {  
    ch <- true     *x = *x + 1     <- ch
} func main() {
    // 注意要設置容量為 1 的緩沖信道     pipline := make(chan bool1)

    var x int     for i:=0;i<1000;i++{
        go increment(pipline, &x)
    }

    // 確保所有的協程都已完成     // 以后會介紹一種更合適的方法(Mutex),這里暫時使用sleep     time.Sleep(3)
    fmt.Println("x 的值:", x)
} 

輸出如下

x 的值:1000 

如果不加鎖,輸出會小于1000。

掃碼二維碼 獲取免費視頻學習資料

Python編程學習

查 看2022高級編程視頻教程免費獲取