1. 程式人生 > >GO語言使用之channel(管道)

GO語言使用之channel(管道)

一、為什麼需要channel

1、需求:
現在要計算 1-200 的各個數的階乘,並且把各個數的階乘放入到map中。最後顯示出來。要求使用goroutine完成

1)、分析思路:
使用goroutine 來完成,效率高,但是會出現併發/並行安全問題.
這裡就提出了不同goroutine如何通訊的問題

2)、程式碼實現
使用goroutine來完成(看看使用gorotine併發完成會出現什麼問題? 然後我們會去解決)
在執行某個程式時,如何知道是否存在資源競爭問題。 方法很簡單,在編譯該程式時,增加一個引數 -race即可
3)、示意圖
這裡寫圖片描述
4)、程式碼實現:

package
utils import ( "sync" "fmt" "time" ) // 需求:現在要計算 1-200 的各個數的階乘,並且把各個數的階乘放入到map中。 // 最後顯示出來。要求使用goroutine完成 // 思路 // 1. 編寫一個函式,來計算各個數的階乘,並放入到 map中. // 2. 我們啟動的協程多個,統計的將結果放入到 map中 // 3. map 應該做出一個全域性的. var ( myMap = make(map[int]int, 10) ) // cacluFactorial 函式就是計算 n!, 讓將這個結果放入到 myMap func cacluFactorial(n int
) { res := 1 for i := 1; i <= n; i++ { res *= i } //這裡我們將 res 放入到myMap myMap[n] = res //concurrent map writes? } func FactorialDemo() { // 我們這裡開啟多個協程完成這個任務[200個] for i := 1; i <= 200; i++ { go cacluFactorial(i) } //休眠10秒鐘【第二個問題 】 time.Sleep(time.Second * 10
) //這裡我們輸出結果,變數這個結果 for i, v := range myMap { fmt.Printf("map[%d]=%d\n", i, v) } }

2、不同goroutine之間如何通訊

  • 全域性變數的互斥鎖
  • 使用管道channel來解決

3、使用全域性變數加鎖同步改程序序
因為沒有對全域性變數 m 加鎖,因此會出現資源爭奪問題,程式碼會出現錯誤,提示concurrent map writes
解決方案:加入互斥鎖
我們的數的階乘很大,結果會越界,可以將求階乘改成 sum += uint64(i)
程式碼改進

package utils


import (
    "sync"
    "fmt"
    "time"
)

// 需求:現在要計算 1-200 的各個數的階乘,並且把各個數的階乘放入到map中。
// 最後顯示出來。要求使用goroutine完成 

// 思路
// 1. 編寫一個函式,來計算各個數的階乘,並放入到 map中.
// 2. 我們啟動的協程多個,統計的將結果放入到 map中
// 3. map 應該做出一個全域性的.

var (
    myMap = make(map[int]int, 10)

    /*同步鎖改進程式碼*/
    //宣告一個全域性互斥鎖
    lock sync.Mutex //sync包提供了基本的同步基元,如互斥鎖。Mutex是一個互斥鎖, 
)

// cacluFactorial 函式就是計算 n!, 讓將這個結果放入到 myMap
func cacluFactorial(n int) {

    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }


    /*同步鎖改進程式碼*/
    lock.Lock()//加鎖

    //這裡我們將 res 放入到myMap
    myMap[n] = res //concurrent map writes?

    lock.Unlock()//解鎖

}

func FactorialDemo() {

    // 我們這裡開啟多個協程完成這個任務[200個]
    for i := 1; i <= 200; i++ {
        go cacluFactorial(i)
    }


    //休眠10秒鐘【第二個問題 】
    time.Sleep(time.Second * 10)

    /*同步鎖改進程式碼*/
    lock.Lock()//加鎖

    //這裡我們輸出結果,變數這個結果
    for i, v := range myMap {
        fmt.Printf("map[%d]=%d\n", i, v)
    }

    lock.Unlock()//解鎖

}

4、為什麼需要channel

前面使用全域性變數加鎖同步來解決goroutine的通訊,但不完美。

主執行緒在等待所有goroutine全部完成的時間很難確定,我們這裡設定10秒,僅僅是估算。

如果主執行緒休眠時間長了,會加長等待時間,如果等待時間短了,可能還有goroutine處於工作狀態,這時也會隨主執行緒的退出而銷燬。

通過全域性變數加鎖同步來實現通訊,也並不利用多個協程對全域性變數的讀寫操作。
上面種種分析都在呼喚一個新的通訊機制-channel

二、channel的基本介紹

1、channle本質就是一個數據結構-佇列
資料是先進先出【FIFO : first in first out】
執行緒安全,多goroutine訪問時,不需要加鎖,就是說channel本身就是執行緒安全的
channel有型別的,一個string的channel只能存放string型別資料。
示意圖:
這裡寫圖片描述
2、定義/宣告channel
語法
var 變數名 chan 資料型別
舉例

var   intChan   chan  int (intChan用於存放int資料)
var   mapChan chan map[int]string (mapChan用於存放map[int]string型別)
var   perChan  chan  Person 
var   perChan2  chan  *Person 


說明
channel是引用型別
channel必須初始化才能寫入資料, 即make後才能使用
管道是有型別的,intChan 只能寫入 整數 int

三、快速入門案例

package utils
import (
    "fmt"
)

//管道的初始化,寫入資料到管道,從管道讀取資料及基本的注意事項
func main() {

    //演示一下管道的使用
    //1. 建立一個可以存放3個int型別的管道
    var intChan chan int
    intChan = make(chan int, 3)

    //2. 看看intChan是什麼
    fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)


    //3. 向管道寫入資料
    intChan<- 10
    num := 211
    intChan<- num
    intChan<- 50
    // intChan<- 98//注意點, 當我們給管寫入資料時,不能超過其容量


    //4. 看看管道的長度和cap(容量)
    fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3

    //5. 從管道中讀取資料

    var num2 int
    num2 = <-intChan 
    fmt.Println("num2=", num2)
    fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3

    //6. 在沒有使用協程的情況下,如果我們的管道資料已經全部取出,再取就會報告 deadlock

    num3 := <-intChan
    num4 := <-intChan
    num5 := <-intChan

    fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)

}

測試結果:

channel len= 3 cap=3
num2= 10
channel len= 2 cap=3
fatal error: all goroutines are asleep - deadlock!

總結:channel使用的注意事項

channel中只能存放指定的資料型別
channle的資料放滿後,就不能再放入了
如果從channel取出資料後,可以繼續放入
在沒有使用協程的情況下,如果channel資料取完了,再取,就會報dead lock

四、channel的遍歷和關閉

1 、channel的關閉
使用內建函式close可以關閉channel, 當channel關閉後,就不能再向channel寫資料了,但是仍然可以從該channel讀取資料

2、channel的遍歷
channel支援for–range的方式進行遍歷,請注意兩個細節
在遍歷時,如果channel沒有關閉,則回出現deadlock的錯誤
在遍歷時,如果channel已經關閉,則會正常遍歷資料,遍歷完後,就會退出遍歷。
3、案列演示

package utils

import (
    "fmt"
)

func TranslateDemo()  {
    intChan := make(chan int, 3)
    intChan<- 100
    intChan<- 200
    close(intChan) // close
    //這是不能夠再寫入數到channel
    //intChan<- 300
    fmt.Println("okook~")
    //當管道關閉後,讀取資料是可以的
    n1 := <-intChan
    fmt.Println("n1=", n1)


    //遍歷管道
    intChan2 := make(chan int, 100)
    for i := 0; i < 100; i++ {
        intChan2<- i * 2  //放入100個數據到管道
    }

    //遍歷管道不能使用普通的 for 迴圈
    // for i := 0; i < len(intChan2); i++ {

    // }
    //在遍歷時,如果channel沒有關閉,則會出現deadlock的錯誤
    //在遍歷時,如果channel已經關閉,則會正常遍歷資料,遍歷完後,就會退出遍歷
    close(intChan2)
    for v := range intChan2 {
        fmt.Println("v=", v)
    }

}

五、 goroutine和channel結合案例

package utils

import (
    "fmt"
)

/*goroutine和channel結合
請完成goroutine和channel協同工作的案例,具體要求:
開啟一個writeData協程,向管道intChan中寫入50個整數.
開啟一個readData協程,從管道intChan中讀取writeData寫入的資料。
注意: writeData和readDate操作的是同一個管道
主執行緒需要等待writeData和readDate協程都完成工作才能退出【管道】


*/
func writeData(intChan chan int) {
    for i := 1; i <= 50; i++ {
        //放入資料
        intChan<- i
        fmt.Println("writeData ", i)
        //time.Sleep(time.Second)
    }
    close(intChan) //關閉
}

//read data
func readData(intChan chan int, exitChan chan bool) {

    for {
        v, ok := <-intChan
        if !ok {
            break
        }
        //time.Sleep(time.Second)
        fmt.Printf("readData 讀到資料=%v\n", v) 
    }
    //readData 讀取完資料後,即任務完成
    exitChan<- true
    close(exitChan)

}

func Test() {

    //建立兩個管道
    intChan := make(chan int, 50)
    exitChan := make(chan bool, 1)

    go writeData(intChan)
    go readData(intChan, exitChan)

    //time.Sleep(time.Second * 10)
    for {
        _, ok := <-exitChan
        if !ok {
            break
        }
    }

}

六、 channel使用細節和注意事項

1、channel可以宣告為只讀,或者只寫性質 【案例演示】
這裡寫圖片描述
2、channel只讀和只寫的最佳實踐案例
這裡寫圖片描述
3、使用select可以解決從管道取資料的阻塞問題【案例演示】

package utils

// 使用select可以解決從管道取資料的阻塞問題

import (
    "fmt"
    "time"
)

func SelectDemo() {

    //使用select可以解決從管道取資料的阻塞問題

    //1.定義一個管道 10個數據int
    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
        intChan<- i
    }
    //2.定義一個管道 5個數據string
    stringChan := make(chan string, 5)
    for i := 0; i < 5; i++ {
        stringChan <- "hello" + fmt.Sprintf("%d", i)
    }

    //傳統的方法在遍歷管道時,如果不關閉會阻塞而導致 deadlock

    //問題,在實際開發中,可能我們不好確定什麼關閉該管道.
    //可以使用select 方式可以解決
    //label:
    for {
        select {
            //注意: 這裡,如果intChan一直沒有關閉,不會一直阻塞而deadlock
            //,會自動到下一個case匹配
            case v := <-intChan : 
                fmt.Printf("從intChan讀取的資料%d\n", v)
                time.Sleep(time.Second)
            case v := <-stringChan :
                fmt.Printf("從stringChan讀取的資料%s\n", v)
                time.Sleep(time.Second)
            default :
                fmt.Printf("都取不到了,不玩了, 程式設計師可以加入邏輯\n")
                time.Sleep(time.Second)
                return 
                //break label
        }
    }
}

4、goroutine中使用recover,解決協程中出現panic,導致程式崩潰問題.【案例演示】

說明: 如果我們起了一個協程,但是這個協程出現了panic, 如果我們沒有捕獲這個panic,就會造成整個程式崩潰,這時我們可以在goroutine中使用recover來捕獲panic, 進行處理,這樣即使這個協程發生的問題,但是主執行緒仍然不受影響,可以繼續執行。

package utils

import (
    "fmt"
    "time"
)

//函式
func sayHello() {
    for i := 0; i < 10; i++ {
        time.Sleep(time.Second)
        fmt.Println("hello,world")
    }
}
//函式
func testRecover() {
    //這裡我們可以使用defer + recover
    defer func() {
        //捕獲test丟擲的panic
        if err := recover(); err != nil {
            fmt.Println("test() 發生錯誤", err)
        }
    }()
    //定義了一個map
    var myMap map[int]string
    myMap[0] = "golang" //error
}

func RecoverDemo() {

    go sayHello()
    go testRecover()


    for i := 0; i < 10; i++ {
        fmt.Println("main() ok=", i)
        time.Sleep(time.Second)
    }

}