目錄
- 引言
- 一、sync.WaitGroup
- 二、channel
- 創建
- channle操作
- 緩沖
- 多返回值模式
- 單向通道
引言
在不做修飾的程序中,代碼是串行執行的
串行、并發與并行串行:事物按照一定的發展順序并發:同一時間段執行多個任務(一邊吃飯一邊看電視)并行:同一時刻執行多個任務(你和你的好朋友都在學習go語言)
對于這樣一段代碼
func hello() {fmt.Println("hello")
}
func main() {hello()fmt.Println("world")
}
輸出
hello
world
如果啟動一個goroutine
func hello() {fmt.Println("hello")
}
func main() {go hello()fmt.Println("world")
}
輸出
world
為什么會出現這樣的結果
這是因為在創建goroutine時需要花費一定時間,在這段時間內,main goroutine是繼續執行的,如果main goroutine執行完成,就不會管其他的goroutine,程序直接退出,所以不會打印出hello
如果想要“hello”也打印出來,就要想辦法讓main goroutine 等一等
最簡單粗暴的方法就是調用time.Sleep函數,延遲結束程序
func hello() {fmt.Println("hello")
}
func main() {hello()fmt.Println("world")time.Sleep(time.Second)
}
編譯執行后打印
world
hello
為什么先打印world?
同樣的,創建goroutine時需要花費一定時間,在這段時間內,main goroutine繼續執行,理解為因為創建goroutine時花費了時間,所以goroutine執行起來比main goroutine慢
但是使用這種方法存在一定的問題,因為不知道程序執行具體需要多長時間
如果sleep時間過長,可能會存在 一段時間內,程序沒有執行任務 的情況,這樣就降低了效率
如果sleep時間過短,還是有可能打印不出goroutine中的語句
要更好的解決這個問題,有三種方法可以使用
- sync.WaitGroup
- channel
- Context
先不說第三種,因為還不會
一、sync.WaitGroup
使用sync.WaitGroup優化上面程序的代碼是
var wg sync.WaitGroup //聲明等待組變量func hello() {fmt.Println("hello")wg.Done() //當前goroutine執行完畢
}func main() {wg.Add(1) //記錄需要等待的goroutine數量go hello()fmt.Println("world")wg.Wait() // 等待直到所有goroutine執行完成
}
編譯執行后打印
world
hello
- sync.WaitGroup定義
type WaitGroup struct {noCopy noCopystate atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.sema uint32
}
不難而見,WaitGroup是一個結構體
sync包中提供了三種WaitGroup的方法
方法名 | 功能 |
---|---|
func (wg *WaitGroup) Add(delta int) | 記錄要等待的協程的數量 |
func (wg *WaitGroup) Done() | 記錄當前協程已執行完畢 |
func (wg *WaitGroup) Wait() | 等待子協程結束,否則阻塞 |
sync.WaitGroup內部可以理解為一個計數器,計數器的值可以增加和減少。
例如當我們啟動了 N 個并發任務時,就將計數器值增加N;
每個任務完成時通過調用 Done 方法將計數器減1;
通過調用 Wait 來等待并發任務執行完,當計數器值為 0 時,表示所有并發任務已經完成
舉一個通俗的例子,這里將計數器稱為count
你室友喊你去打游戲,你給自己定了兩個目標:復習二重積分和三重積分,完成這兩個目標就去和室友打游戲
首先,你給自己定了兩個目標(count = 2),復習完二重積分后,你還剩下一個目標沒有完成(count = 1),這時候你的室友還要繼續等你(wait),好了,又把三重積分復習完了(count = 0),這時候你就和室友去打游戲了
WaitGroup 通常適用于可動態調整協程數量的時候,例如事先知道協程的數量,又或者在運行過程中需要動態調整。
WaitGroup是結構體,作為函數參數傳參時,應該傳遞指針而不是值;如果傳遞值,只是將WaitGroup拷貝后,對拷貝的WaitGroup進行修改,不會改變真正的WaitGroup的值,這可能會導致主協程一直阻塞等待,程序將無法正常運行
二、channel
這一部分是我在學習channel 時候的筆記加上自己的理解
參考🔗李文周的博客-Go語言基礎之并發 🔗Golang 中文學習文檔-并發
創建
- 聲明
var 變量名稱 chan 元素類型
chan:關鍵字
元素類型:是指通道中傳遞元素的類型
var ch chan int //傳遞整型的通道var ch1 chan string //傳遞string類型的通道
沒有初始化之前通道是對應類型的零值
- 創建
用make創建
ch1 := make(chan int) //沒有緩沖區(下面說緩沖)ch2 := make(chan int, 1) //緩沖區為1
- 關閉
使用內置的 close 關閉通道
close定義:
func close(c chan<- Type)
關閉ch通道:
close(ch)
用戶必須發出一個關閉通道的指令,通道才會關閉
與文件操作不同,文件在結束操作后必須關閉,但是通道不必須關閉
channle操作
通道的操作有 發送(send)、接收(receive)和關閉(close)。
發送和接收都用符號 ‘<-’
- 發送
ch <-:表示對一個通道寫入數據
ch <- 5 // 把5發送到ch中
- 接收
<- ch:表示對一個通道讀取數據(直接看箭頭指向區分這兩種操作就可以)
- 單返回值
x := <- ch // 從ch中接收值并賦值給變量x
<-ch // 從ch中接收值,忽略結果
- 雙返回值
value, ok := <-ch
value:通道中的值,如果被關閉返回對應的零值
ok:布爾類型的值,通道關閉時返回false,否則返回true
雙返回值還可以用來判斷通道是否關閉
-
判斷通道是否被關閉:
示例:value, ok := <-ch
value:從通道中取出的值,如果通道被關閉則返回對應類型的零值
ok:通道ch關閉時返回false,否則返回true -
for range 接收值
通常用for range循環從通道中接收值
如果通道被關閉,會 在通道內的所有制被接收完畢后 自動退出循環
如果沒有關閉,使用for range執行時會出錯
ch4 := make(chan int, 4)ch4 <- 1ch4 <- 2ch4 <- 3ch4 <- 4close(ch4)for value := range ch4 {fmt.Println(value)}}func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)
}
輸出
1
2
3
4
通道的發送接收操作可以理解為一個容器
如果容器有空間,就可以把物品放進容器;
如果容器空間滿了,在容器中取出物品后,容器空間又有剩余,又可以把其他物品放入容器
關閉后的通道有以下特點:
- 對一個關閉的通道再發送值就會導致panic
- 對一個關閉的通道繼續接收會一直獲取值直到通道為空
- 對一個關閉的并且沒有值的通道執行接收操作會得到對應類型的零值
- 關閉一個已經關閉的通道會導致panic
- 對已經關閉的通道再執行close也會引發panic
緩沖
- 無緩沖的通道(阻塞的通道)
ch1 := make(chan int) //ch1是一個無緩沖的通道ch1 <- 10fmt.Println("發送成功")
go fatal error: all goroutines are asleep - deadlock!
deadlock -- 表示程序中的goroutine都被掛起導致程序死鎖了對一個無緩沖區通道執行發送操作,會發生阻塞對一個無緩沖通道執行接收操作,沒有任何向通道中發送值的操作也會導致接受操作阻塞
應對阻塞通道的方法
- 創建goroutine
func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)}func main(){ch2 := make(chan int)go recv(ch2)ch2 <- 10 // 發送操作fmt.Println("發送成功")close(ch2)//fmt.Println(<-ch2) -- 報錯了}
這段代碼的過程
case1:如果先進行發送操作,發生堵塞,直到另一個goroutine執行接收操作
case2:如果先進行接收操作,發生堵塞,直到另一個goroutine執行發送操作
- 使用有緩沖的通道
ch3 := make(chan int, 1)ch3 <- 1fmt.Println("發送成功 ")x1 := <-ch3fmt.Println(x1) //輸出1ch3 <- 2close(ch3)//對關閉的通道執行接收操作,會直到取完通道中的元素num0 := len(ch3) //len--獲取通道中元素個數x2 := <-ch3num := len(ch3)fmt.Println(num0, x2, num) //輸出 1 2 0
只要通道的容量大于0,那就是有緩沖的通道,通道的容量表示通道中最大能存放的元素數量。
當通道內一鈾元素達到最大容量后,再向通道中執行發送操作就會阻塞(如果接收后再發送就不會了,相當于清空了)
len -- 獲取通道內元素的數量
cap -- 獲取通道的容量
多返回值模式
-
判斷通道是否被關閉:
示例:value, ok := <-ch
value:從通道中取出的值,如果通道被關閉則返回對應類型的零值
ok:通道ch關閉時返回false,否則返回true -
for range 接收值
通常用for range循環從通道中接收值
如果通道被關閉,會 在通道內的所有制被接收完畢后 自動退出循環
如果沒有關閉,使用for range執行時會出錯
ch4 := make(chan int, 4)ch4 <- 1ch4 <- 2ch4 <- 3ch4 <- 4close(ch4)for value := range ch4 {fmt.Println(value)}}func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)
}
輸出
1
2
3
4
單向通道
現在有兩個函數
producer 函數 返回通道,并且執行發送操作
consume r函數從通道中接收值進行計算
func Producer() chan int {ch := make(chan int) //有沒有緩沖值都可以//創建一個新的goroutine執行發送數據的任務go func() {for i := 0; i < 5; i++ {ch<-i}}close(ch)}()return ch
}func Consumer(ch chan int) int {sum := 0for value := range ch {sum += value}return sum
}
上面的代碼沒辦法阻止在接收通道中執行發送操作,同理,沒辦法阻止在發送通道中執行接收操作
可以 限制參數或者返回值 來限制函數
<-chan int //只接收通道,只能接收不能發送
chan <-int //只發送通道,只能發送不能接收
改寫成
func Producer1() <-chan int { //發送操作ch := make(chan int) //有沒有緩沖值都可以//創建一個新的goroutine執行發送數據的任務go func() {for i := 0; i < 10; i++ {//有 1 3 5 7 9if i%2 == 1 {ch <- i}}close(ch)}()return ch
}func Consumer1(ch <-chan int) int { //接收操作sum := 0for value := range ch {sum += value}return sum
}func main() {//在函數傳參及任何賦值操作中全向通道(正常通道)可以轉換為單向通道,但是沒辦法反向轉換(單項通道沒辦法轉換成全向通道)ch1 := make(chan int, 1)ch1 <- 10close(ch1)Consumer1(ch1) //在傳參時將ch1轉為單項通道ch2 := make(chan int, 1)ch2 <- 4 //向ch2中發送4var ch3 <-chan int //聲明一個通道 只接收ch3 = ch2 //變量賦值時將ch2轉換為單向通道<-ch3 //接收操作
}