1. 程式人生 > >Go--關於 goroutine、channel

Go--關於 goroutine、channel

## Go--關於 goroutine、channel ### goroutine ==協程==是一種**輕量化的執行緒**,由`Go`編譯器進行優化。 `Go`協程具有以下特點: - 有獨立的棧空間 - 共享程式堆中的空間 - 排程由使用者控制 如果主執行緒`main`函式(主 `goroutine`或者`main goroutine`)返回或者退出時,即使所有協程(`goroutine`)還沒執行完畢,也會退出。當然,協程可以在主執行緒未退出之前自己執行完畢,並退出。 --- - 主執行緒是一個物理執行緒,直接作用在`cpu`上。是重量級的,非常耗費`cpu`資源。 - 協程從主執行緒開啟的,是輕量級的執行緒,是邏輯態的。對資源要求相對較小。 - `Golang`可以開啟成千上萬個協程。這是`Golang`的併發優勢。 --- #### MPG模式 ![image-20201101201149692](https://i.loli.net/2020/11/01/7CUmVwRlWPYo5qO.png) ![image-20201101201058123](https://i.loli.net/2020/11/01/MeqIyYEhK4Pd9T6.png) ![image-20201101201034227](https://i.loli.net/2020/11/01/bZKL3iQ2ENVWxrz.png) --- - `Go`1.8後,預設讓程式執行在多個核上,可以不用設定了 - `Go`1.8前,還是要設定一下,可以更高效的利益`cpu` ~~~go numsCPU :=runtime.NumCPU() //獲取系統CPU數 runtime.GOMAXPROCS(numsCPU) //設定執行的CPU數目 ~~~ --- ### channel 在此之前,先說明一種實現同步的方式:==加鎖==(注意這裡說的指**互斥鎖**) 需求:計算` n! `: ~~~go var lock sync.Mutex //使用全域性變數加鎖 func testInput(n int) { res := 1 for i := 1; i <= n; i++ { res *= i } lock.Lock() myMap[n] = uint64(res) lock.Unlock() } func main() { for i := 1; i <= 50; i++ { go testInput(i) } time.Sleep(time.Second *5) //不等待會提前結束計算,未計算的執行緒將被退出 lock.Lock() for i,v := range myMap { fmt.Println("map[",i,"]=",v) } lock.Unlock() } ~~~ 通過加互斥鎖(同步鎖)的方式,併發進行運算、新增,但是這種方式也有缺點: - 前面使用全域性變數加鎖同步來解決`goroutine`的通訊,但不完美 - 主執行緒在等待所有`goroutine`全部完成的時間很難確定,我們這裡設定 5 秒,僅僅是估算。 - 如果主執行緒休眠時間長了,會加長等待時間,如果等待時間短了,可能還有`goroutine` 處於工作狀態,這時也會隨主執行緒的退出而銷燬。 - 通過全域性變數加鎖同步來實現通訊,也並不利用多個協程對全域性變數的讀寫操作。 --- 還可以用`channel`來解決: ~~~go func add(s []int , c chan int) { sum := 0 for _, v := range s { sum += v fmt.Println(v) } c <- sum } func main() { c := make(chan int) s :=[]int{2,5,9,23,7,3,4} go add(s[:len(s)/2 ] ,c) //寫channel操作會阻塞,直到讀channel操作執行 go add( s[len(s)/2:] ,c) x ,y:= <-c ,<-c //隨機併發 fmt.Println(x ,y ,x+y) } ~~~ 通道是帶有型別的管道,你可以通過它用通道操作符 <- 來發送或者接收值。 ~~~go ch <- v // 將 v 傳送至通道 ch。 v := <-ch // 從 ch 接收值並賦予 v。 ~~~ (“箭頭”就是資料流的方向。) 和對映與切片一樣,通道在使用前必須建立: ~~~go ch := make(chan int) ~~~ 預設情況下,傳送和接收操作在另一端準備好之前都會阻塞。這使得 `Go` 程可以在沒有顯式的鎖或競態變數的情況下進行同步。 - `channel` 是執行緒安全的; - `channel` 本質是佇列,遵循**先進先出**; - `channel` 中只能存放指定的資料型別; - `channel` 的資料放滿後,就不能再放入; - 如果從`channel` 取出資料後,可以繼續放入; - 在沒有使用協程的情況下,如果`channel` 資料取完了,再取,就會報`deadlock`。 --- 還可以放進任意型別(`interface{}`)的資料: ~~~go package main import "fmt" type Student struct { Name string `json:"name"` // 是 ` ` (tab鍵上的~按鍵) ,不是 ' ' Sex string `json:"sex"` } func main() { allChan := make(chan interface{},5) stu1 := Student{Name: "lili",Sex: "f"} stu2 := Student{Name: "chang",Sex: "m"} stu3 := Student{Name: "ling",Sex: "m"} allChan <- stu1 allChan <- 10 allChan <- stu2 allChan <- 99.5 allChan <- stu3 <- allChan <- allChan stuRes := <- allChan fmt.Println(stuRes) //讀取結構體型別資料欄位,需要先進行型別斷言 stu := stuRes.(Student) fmt.Println(stu.Name) fmt.Println(stu.Sex) } ~~~ 由於`channel`是`interface{}`型別,所以使用的時候,都需要先進行型別斷言。 ~~~go allChan <- myMap <- allChan <- allChan stuMap := <- allChan stus := stuMap.(map[int]Student) fmt.Println(stus) fmt.Println(stus[0]) fmt.Println(stus[1]) ~~~ ![image-20201101235011061](https://i.loli.net/2020/11/02/FNmfjIq9D7WAnsR.png) ~~~go allChan <- stu1 allChan <- 10 allChan <- stu2 allChan <- 99.5 allChan <- stu3 <- allChan n := <- allChan n += 1 //報錯 ~~~ ![image-20201102124504467](https://i.loli.net/2020/11/02/j56XmuGeVqA8KZa.png) 不使用型別斷言,直接使用將會報錯。因為編譯器並不認識此型別,需要經過型別斷言進行確認。 --- #### channel的關閉 `channel`關閉使用 `close(chan)`,關閉`channel`。 關閉後不能再向`channel`傳送資料,只能從`channel`讀取資料。 在上面的例子中的`<-allChan`**前**加入以下程式碼: ~~~go close(allChan) ~~~ ![image-20201102130028586](https://i.loli.net/2020/11/02/KHatcZbW4Uo1nVv.png) --- #### channel的遍歷 遍歷`channel`之前需要關閉`channel`,否則會報錯(`deadlock`)。 ![image-20201102130608550](https://i.loli.net/2020/11/02/97YGCqc2b65pgwd.png) 關閉`channel`後,即可正常進行遍歷`channel`,知道遍歷完成,退出遍歷。 ~~~go close(allChan) for v := range allChan { fmt.Println(v) } ~~~ --- #### 只讀、只寫 channel - 只讀`channel`:(例如) ~~~go var chan1 <-chan int ~~~ - 只寫`channel`:(例如) ~~~go var chan2 chan<- int ~~~ 案例: ~~~go func send(ch chan<- int,exit chan struct{}) { for i := 0; i < 10; i++ { ch <- i fmt.Println("輸入",i) } close(ch) var a struct{} exit <- a } func get(ch <-chan int,exit chan struct{}) { for i := 0; i < 10; i++ { v,ok := <-ch if !ok { break } fmt.Println("輸出",v) } var a struct{} exit <- a } func main() { ch := make(chan int ,10) //雙向通道 exitChan := make(chan struct{} ,2) go send(ch,exitChan) go get(ch,exitChan) for { if len(exitChan) == 2 { break } } } ~~~ ![image-20201102144957098ddd](https://i.loli.net/2020/11/02/t7CpSFPzA3aEgZ2.png) --- 可以使用`for`+`select`語句防止阻塞: ~~~go func main() { ch := make(chan int ,10) for i := 0; i < 5; i++ { ch <- i } ch2 := make(chan float64 ,10) for i := 0; i < 5; i++ { ch2 <- rand.Float64() } label: for { select { case n:= <-ch: fmt.Println(n) time.Sleep(time.Second) case m:=<-ch2: fmt.Println(m) time.Sleep(time.Second) default: fmt.Println("沒了") time.Sleep(time.Second) //return 直接結束退出程式執行 break label //中斷指定的 for 迴圈 } } } ~~~ ![image-20201102144957098](https://i.loli.net/2020/11/02/3UMv4QYoIFWGqS1.png) --- 使用`defer`+`recover`解決執行時協程中丟擲的`panic`,保證程式繼續執行: ~~~go func wrong() { defer func(){ if e := recover() ; e != nil { fmt.Print("func wrong()計算錯誤,") fmt.Println("異常",e) } }() num := 0 num1 := 100 num = num1 /num fmt.Println("func wrong()計算正確",num) } func right() { defer func(){ if e := recover() ; e != nil { fmt.Println(e) } }() num := 10 num1 := 100 num = num1 /num fmt.Println("func right() 計算正確",num) } func main() { go wrong() go right() for i := 0; i < 5; i++ { time.Sleep(time.Second) i++ } } ~~~ ![image-20201102151030206](https://i.loli.net/2020/11/02/T9gcHlQijPb5Gh7.png) 關於`recover`: >內建函式recover允許程式管理恐慌過程中的Go程。在defer的函式中,執行recover呼叫會取回傳至panic呼叫的錯誤值,恢復正常執行,停止恐慌過程。若recover在defer的函式之外被呼叫,它將不會停止恐慌過程式列。在此情況下,或當該Go程不在恐慌過程中時,或提供給panic的實參為nil時,recover就會返回nil