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

Hi,你們好,我是明哥。git

在本身學習 Golang 的這段時間裏,我寫了詳細的學習筆記放在個人我的微信公衆號 《Go編程時光》,對於 Go 語言,我也算是個初學者,所以寫的東西應該會比較適合剛接觸的同窗,若是你也是剛學習 Go 語言,不防關注一下,一塊兒學習,一塊兒成長。github

個人在線博客:golang.iswbm.com 個人 Github:github.com/iswbm/GolangCodingTimegolang


Go 語言之因此開始流行起來,很大一部分緣由是由於它自帶的併發機制。編程

若是說 goroutine 是 Go語言程序的併發體的話,那麼 channel(信道) 就是 它們之間的通訊機制。channel,是一個可讓一個 goroutine 與另外一個 goroutine 傳輸信息的通道,我把他叫作信道,也有人將其翻譯成通道,兩者都是一個概念。數組

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

1. 信道的定義與使用

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

var 信道實例 chan 信道類型

// 定義容量爲10的信道
var 信道實例 [10]chan 信道類型複製代碼

聲明後的信道,其零值是nil,沒法直接使用,必須配合make函進行初始化。數據結構

信道實例 = make(chan 信道類型)複製代碼

亦或者,上面兩行能夠合併成一句,如下我都使用這樣的方式進行信道的聲明併發

信道實例 := make(chan 信道類型)複製代碼

假如我要建立一個能夠傳輸int類型的信道,能夠這樣子寫。異步

// 定義信道
pipline := make(chan int)複製代碼

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

// 往信道中發送數據
pipline<- 200

// 從信道中取出數據,並賦值給mydata
mydata := <-pipline複製代碼

信道用完了,能夠對其進行關閉,避免有人一直在等待。可是你關閉信道後,接收方仍然能夠從信道中取到數據,只是接收到的會永遠是 0。

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 int, 10)
    fmt.Printf("信道可緩衝 %d 個數據\n", cap(pipline))
    pipline<- 1
    fmt.Printf("信道中當前有 %d 個數據", len(pipline))
}複製代碼

輸出以下

信道可緩衝 10 個數據
信道中當前有 1 個數據複製代碼

3. 緩衝信道與無緩衝信道

按照是否可緩衝數據可分爲:緩衝信道無緩衝信道

緩衝信道

容許信道里存儲一個或多個數據,這意味着,設置了緩衝區後,發送端和接收端能夠處於異步的狀態。

pipline := make(chan int, 10)複製代碼

無緩衝信道

在信道里沒法存儲數據,這意味着,接收端必須先於發送端準備好,以確保你發送完數據後,有人立馬接收數據,不然發送端就會形成阻塞,緣由很簡單,信道中沒法存儲數據。也就是說發送端和接收端是同步運行的。

pipline := make(chan int)

// 或者
pipline := make(chan int, 0)複製代碼

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 := 1, 1
    for i := 0; i < n; i++ {
        mychan <- x
        x, y = y, x+y
    }
    // 記得 close 信道
    // 否則主函數中遍歷完並不會結束,而是會阻塞。
    close(mychan)
}

func main() {
    pipline := make(chan int, 10)

    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 bool, 1)

    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。

系列導讀

01. 開發環境的搭建(Goland & VS Code)

02. 學習五種變量建立的方法

03. 詳解數據類型:**整形與浮點型**

04. 詳解數據類型:byte、rune與string

05. 詳解數據類型:數組與切片

06. 詳解數據類型:字典與布爾類型

07. 詳解數據類型:指針

08. 面向對象編程:結構體與繼承

09. 一篇文章理解 Go 裏的函數

10. Go語言流程控制:if-else 條件語句

11. Go語言流程控制:switch-case 選擇語句

12. Go語言流程控制:for 循環語句

13. Go語言流程控制:goto 無條件跳轉

14. Go語言流程控制:defer 延遲調用

15. 面向對象編程:接口與多態

16. 關鍵字:make 和 new 的區別?

17. 一篇文章理解 Go 裏的語句塊與做用域

18. 學習 Go 協程:goroutine

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

20. 幾個信道死鎖經典錯誤案例詳解

21. 學習 Go 協程:WaitGroup

22. 學習 Go 協程:互斥鎖和讀寫鎖

23. Go 裏的異常處理:panic 和 recover

24. 超詳細解讀 Go Modules 前世此生及入門使用

25. Go 語言中關於包導入必學的 8 個知識點

26. 如何開源本身寫的模塊給別人用?

27. 說說 Go 語言中的類型斷言?

28. 這五點帶你理解Go語言的select用法


相關文章
相關標籤/搜索