Go--關於 goroutine、channel
goroutine
協程是一種輕量化的執行緒,由Go
編譯器進行優化。
Go
協程具有以下特點:
- 有獨立的棧空間
- 共享程式堆中的空間
- 排程由使用者控制
如果主執行緒main
函式(主 goroutine
或者main goroutine
)返回或者退出時,即使所有協程(goroutine
)還沒執行完畢,也會退出。當然,協程可以在主執行緒未退出之前自己執行完畢,並退出。
- 主執行緒是一個物理執行緒,直接作用在
cpu
上。是重量級的,非常耗費cpu
資源。 - 協程從主執行緒開啟的,是輕量級的執行緒,是邏輯態的。對資源要求相對較小。
Golang
可以開啟成千上萬個協程。這是Golang
的併發優勢。
MPG模式
Go
1.8後,預設讓程式執行在多個核上,可以不用設定了Go
1.8前,還是要設定一下,可以更高效的利益cpu
numsCPU :=runtime.NumCPU() //獲取系統CPU數
runtime.GOMAXPROCS(numsCPU) //設定執行的CPU數目
channel
在此之前,先說明一種實現同步的方式:加鎖(注意這裡說的指互斥鎖)
需求:計算n!
:
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
來解決:
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)
}
通道是帶有型別的管道,你可以通過它用通道操作符 <- 來傳送或者接收值。
ch <- v // 將 v 傳送至通道 ch。
v := <-ch // 從 ch 接收值並賦予 v。
(“箭頭”就是資料流的方向。)
和對映與切片一樣,通道在使用前必須建立:
ch := make(chan int)
預設情況下,傳送和接收操作在另一端準備好之前都會阻塞。這使得 Go
程可以在沒有顯式的鎖或競態變數的情況下進行同步。
-
channel
是執行緒安全的; -
channel
本質是佇列,遵循先進先出; -
channel
中只能存放指定的資料型別; -
channel
的資料放滿後,就不能再放入; -
如果從
channel
取出資料後,可以繼續放入; -
在沒有使用協程的情況下,如果
channel
資料取完了,再取,就會報deadlock
。
還可以放進任意型別(interface{}
)的資料:
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{}
型別,所以使用的時候,都需要先進行型別斷言。
allChan <- myMap
<- allChan
<- allChan
stuMap := <- allChan
stus := stuMap.(map[int]Student)
fmt.Println(stus)
fmt.Println(stus[0])
fmt.Println(stus[1])
allChan <- stu1
allChan <- 10
allChan <- stu2
allChan <- 99.5
allChan <- stu3
<- allChan
n := <- allChan
n += 1 //報錯
不使用型別斷言,直接使用將會報錯。因為編譯器並不認識此型別,需要經過型別斷言進行確認。
channel的關閉
channel
關閉使用 close(chan)
,關閉channel
。
關閉後不能再向channel
傳送資料,只能從channel
讀取資料。
在上面的例子中的<-allChan
前加入以下程式碼:
close(allChan)
channel的遍歷
遍歷channel
之前需要關閉channel
,否則會報錯(deadlock
)。
關閉channel
後,即可正常進行遍歷channel
,知道遍歷完成,退出遍歷。
close(allChan)
for v := range allChan {
fmt.Println(v)
}
只讀、只寫 channel
- 只讀
channel
:(例如)
var chan1 <-chan int
- 只寫
channel
:(例如)
var chan2 chan<- int
案例:
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
}
}
}
可以使用for
+select
語句防止阻塞:
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 迴圈
}
}
}
使用defer
+recover
解決執行時協程中丟擲的panic
,保證程式繼續執行:
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++
}
}
關於recover
:
內建函式recover允許程式管理恐慌過程中的Go程。在defer的函式中,執行recover呼叫會取回傳至panic呼叫的錯誤值,恢復正常執行,停止恐慌過程。若recover在defer的函式之外被呼叫,它將不會停止恐慌過程式列。在此情況下,或當該Go程不在恐慌過程中時,或提供給panic的實參為nil時,recover就會返回nil。