(四十二)golang--管道
假設我們現在有這麼一個需求:
計算1-200之間各個數的階乘,並將每個結果儲存在mao中,最終顯示出來,要求使用goroutime。
分析:
(1)使用goroutime完成,效率高,但是會出現併發/並行安全問題;
(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 } } }
結果: