(四十三)golang--管道


假設我們現在有這么一個需求:

計算1-200之間各個數的階乘,並將每個結果保存在map中,最終顯示出來,要求使用goroutine。

分析:

(1)使用goroutine完成,效率高,但是會出現並發/並行安全問題;

(2)不同協程之間如何通信;

  • 對於(1):不同協程之間可能同時對一塊內存進行操作,導致數據的混亂,即並發/並行不安全;主協程運行完了,計算階乘的協程卻沒有運行完,功能並不能夠准確實現;可利用互斥鎖解決該問題;
  • 對於(2):可以利用利用管道;

正常的代碼:

package main

import (
    "fmt"
    "sync"
)

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

func cal(n int) {
    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }
    myMap[n] = res
}

func main() {
    for i := 1; i <= 15; i++ {
        go cal(i)
    }
    for i, v := range myMap {
        fmt.Printf("map[%d]=%d\n", i, v)
    }
}

運行結果:會報錯

1.利用互斥鎖 

package main

import (
    "fmt"
    "sync"
  ""  
) var ( myMap = make(map[int]int, 10) //lock是全局互斥鎖,synchornized lock sync.Mutex ) func cal(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } lock.Lock() myMap[n] = res lock.Unlock() } func main() { for i := 1; i <= 15; i++ { go cal(i) } for i, v := range myMap { fmt.Printf("map[%d]=%d\n", i, v) } }

有可能主程序運行完了而cal還沒運行完(上面結果只到13,沒有14,15),需要加上time.Sleep(time.Seconde*3),而在輸出時,由於主協程並不知道程序已經完成了,底層仍然可能出現競爭資源,所以在輸出階段也要加上互斥鎖。最終代碼如下:

package main

import (
    "fmt"
    "sync"
)

var (
    myMap = make(map[int]int, 10)
    //lock是全局互斥鎖,synchornized
    lock sync.Mutex
)

func cal(n int) {
    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }
    lock.Lock()
    myMap[n] = res
    lock.Unlock()
}

func main() {
    for i := 1; i <= 15; i++ {
        go cal(i)
    }

    time.Sleep(time.Second * 4)

    lock.Lock() for i, v := range myMap {
        fmt.Printf("map[%d]=%d\n", i, v)
    }
    lock.Unlock()
}

為什么需要管道?

(1)主線程在等待所有協程全部完成的時間很難確定;

(2)如果主線程休眠時間長了,會加長等待時間,如果等待時間短了,可能協程還處於工作狀態,這時也會隨着主協程的結束而銷毀;

(3)通過全局變量加鎖同步來實現通訊,也並不利於多個協程對全局變量的讀寫操作;

管道的介紹:
(1)管道的本質就是一種數據結構--隊列;

(2)數據先進先出;

(3)線程安全,多協程訪問時,不需要加鎖;

(4)管道只能存儲相同的數據類型;

管道的聲明:

var intChan chan int;

var stringChan chan string;

var mapChan chan map[int]string;

var perChan chan Person;

var perChan chan *Person;

注意:管道是引用類型;管道必須初始化后才能寫入數據;管道是有類型的,即IntChan只能寫入int;

管道初始化:

var intChan chan int

intChan = make(chan int,10) 

向管道中讀寫數據:

num := 10

intChan<-num

var num2 int

num2<-intChan

注意:管道容量滿了則不能繼續寫入,在沒有使用協程的情況下,管道空了不能繼續讀取。

如何使管道中存儲任意數據類型?

channel的關閉:

使用內置的close可以關閉管道,關閉后不能再進行寫入,但是可以進行讀取;

channel的遍歷:

channel可以使用for range進行遍歷 ,但是要注意:

  • 在遍歷時,如果channel沒有關閉,則會出現deadlock錯誤;
  • 在遍歷時,如果channel已經關閉,則會正常遍歷數據,遍歷完成后退出;(即在遍歷前需要先關閉管道)

2.利用管道實現邊寫邊讀

流程圖:

package main

import (
    "fmt"
)

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

func cal(n int) map[int]int {
    res := 1
    for i := 1; i <= n; i++ {
        res *= i
    }
    myMap[n] = res
    return myMap
}

func write(myChan chan map[int]int) {
    for i := 0; i <= 15; i++ {
        myChan <- cal(i)
        fmt.Println("writer data:", cal(i))
    }
    close(myChan)
}

func read(myChan chan map[int]int, exitChan chan bool) {
    for {
        v, ok := <-myChan
        if !ok {
            break
        }
        fmt.Println("read data:", v)
    }
    exitChan <- true
    close(exitChan)
}

func main() {
    var myChan chan map[int]int
    myChan = make(chan map[int]int, 20)
    var exitChan chan bool
    exitChan = make(chan bool, 1)
    go write(myChan)
    go read(myChan, exitChan)
    for {
        _, ok := <-exitChan
        if !ok {
            break
        }
    }

}

結果:

思考:假設我們注銷掉go read(myChan,exitChan)會發生什么呢?

也就是說,只有寫入myChan而沒有讀取myChan,當存入myChan里面的數據達到了myChan的容量,再繼續存入就會報deadlock錯誤。同時,由於exitChan需要寫入一個true,而exitChan需要讀取完myChan中的數據后才寫入一個true,但是現在不能進行讀取,也就是說,true不會寫入exitChan,就形成了阻塞。假設我們打開go read(myChan,exitChan),我們設置其每隔1秒才讀取一條數據,而寫入則讓其正常運行,也就是說,寫入很快,讀取很慢,這樣會導致deadlock嗎?答案是不會,只要有讀取,golang會有個機制,不會讓myChan存儲的值超過myChan的容量。

管道的使用注意事項:

(1)在默認情況下,管道是雙向的。管道是可以聲明是只讀還是只寫;

  var intChan chan<-int(只寫)

  intChan = make(chan int,3)

    var intChan2 <-chan int

(2)使用select可以解決從管道取數據阻塞問題;

func Test2() {

    intChan := make(chan int, 10)
    for i := 0; i < 10; i++ {
        intChan <- i
    }
    strChan := make(chan string, 5)
    for i := 0; i < 5; i++ {
        strChan <- "hello" + fmt.Sprintf("%d", i)
    }
    //傳統方法是可用close關閉,但是當不知道什么時候需要關閉時,這就不可用
    //實際開發中可以使用select解決
    for {
        select {
        case v := <-intChan:
            fmt.Printf("從intChan中讀取數據%d\n", v)
        case v := <-strChan:
            fmt.Printf("從strChan中讀取數據%s\n", v)
        default:
            fmt.Println("都取不到數據了")
            return
        }
    }

}

運行結果:

 (4)goroutine中使用recover,解決協程中出現panic,導致程序崩潰問題。

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

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 10; i++ {
        time.Sleep(time.Millisecond * 3)
        fmt.Println("hello")
    }

}

func test() {
    //這里我們可以使用defer revover解決nil
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("test()發生錯誤,error=", err)
        }
    }()
    var myMap map[int]string
    myMap[0] = "golang"
}
func main() {
    go sayHello()
    go test()
    for i := 0; i < 10; i++ {
        time.Sleep(time.Millisecond * 3)
        fmt.Println("main() ok=", i)

    }
}

運行結果:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM