Golang控制通道實現協(xié)程等待詳解
前言
上一次簡單了解了協(xié)程的工作原理 前文鏈接
最后提到了幾個使用協(xié)程時會遇到的問題,其中一個就是主線程不會等待子線程結束,在這里記錄兩種比較簡單的方法,并借此熟悉下通道的概念。
方法一-睡眠等待
簡單暴力的解決方案,在創(chuàng)建了子協(xié)程之后,主協(xié)程等待一段時間再結束。
func goroutineTest(i int) {
fmt.Println(i)
}
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
//等待一段時間
time.Sleep(time.Millisecond * 500)
}簡單暴力但是并不實用,缺點十分明顯, 我們并不知道子協(xié)程什么時候會全部執(zhí)行結束,要么等待時間太短,要么等待時間太長影響方法執(zhí)行性能。
所以我們需要其他更靈活的方式。
方法二-通道
什么是通道
通道是 Go 自帶的、唯一的可以滿足并發(fā)安全的類型,通道相當于是一個先進先出的隊列,通道中的元素會按照插入進來時候的順序再發(fā)送出去。
通道的特性
一、進出通道的值都是副本數據
當向通道內傳值時,傳遞的其實是原本元素的副本,移動時也是同理。
二、對于同一個通道,發(fā)送操作之間是互斥的,接收操作之間也是互斥的。
當有操作要向通道發(fā)送數據的時候,其他操作的發(fā)送處理會被阻塞,只有當前面的操作執(zhí)行完畢,值完全被復制進通道內之后,后面的操作才可以發(fā)送。接收通道里的值也是同理,只有當值被接受,且元素在通道內被刪除之后,其他的接受操作才能被執(zhí)行。
三、對于同一個通道同一元素值來說,發(fā)送和接受操作也是互斥的。
因為上面提到元素進出通道是通過副本的方式,這樣做可以避免出現復制還未完成就有操作將其取走的情況。
四、發(fā)送操作和接收操作中對元素值的處理是絕對完整的。
發(fā)送至通道的操作時,絕不會出現只復制一部分的情況,同理接收通道時,通道在準備好元素值的副本之后一定會將通道內的原值刪除。
五、發(fā)送和接受操作在未完成之前會一直阻塞
其實就是為了實現操作的互斥和元素值的完整。
什么是非緩沖通道
無緩沖隊列表示長度為 0 的通道,長度為 0 表示這個通道不能保留信息,數據是直接從發(fā)送方復制到接收方,當有信息傳進來時,發(fā)送方會阻塞,直到有接收方接受這個值,當然我們也可以先找到一個接收方去接收(但是這里有個坑>>>需要注意死鎖)。
什么是緩沖通道
緩沖隊列就是表示長度大于 0 的通道,這里的通道相當于一個中轉倉庫。如果通道滿了,那么對它的所有發(fā)送操作都會被阻塞,直到通道中有元素值被接收走,通道會優(yōu)先通知最早因此在等待的發(fā)送操作。同理,如果通道已空,那么對它的所有接收操作都會被阻塞,直到通道中有新的元素值出現。這時,通道會通知最早等待的那個接收操作。
通道的簡單使用
- 在聲明通道時,我們要定義通道內元素的類型 chan [type]
- <- 是發(fā)送至通道和接收通道元素的操作符,通道在左側表示向通道內發(fā)送,通道在右側表示從通道內接收
- 通道定義之后,一定要初始化,對于沒有進行初始化的通道,發(fā)送和接收操作都是阻塞的
非緩沖通道
定義一個非緩沖通道,通道內元素類型為 int
var ch0 = make(chan int)
func set() {
for i := 0; i < 3; i++ {
time.Sleep(time.Millisecond * 1000)
fmt.Println(i)
}
ch0 <- 999
}
func main() {
//val := <-ch0
go set()
fmt.Println("work in here")
val2 := <-ch0
fmt.Println("get value")
fmt.Println(val2)
fmt.Println("work finished")
}
這段代碼主協(xié)程中先開啟了一個子協(xié)程,然后從通道中接受一個值,并打印出來,運行結果如下。
主協(xié)程執(zhí)行到 val2 := <-ch0 這一行時發(fā)生了阻塞,三秒過后子協(xié)程向通道中發(fā)送了元素值。主協(xié)程從通道中接收到了值,然后繼續(xù)向下走。
work in here
0
1
2
get value
999
work finished
緩沖通道
func main() {
定義一個大小為?。场〉耐ǖ?
chn := make(chan int, 3)
chn <- 1
chn <- 2
chn <- 3
fmt.Printf("get value: %v\n", <-chn)
fmt.Printf("get value: %v\n", <-chn)
fmt.Printf("get value: %v\n", <-chn)
}
get value: 1
get value: 2
get value: 3
小心死鎖
有一個需要注意的地方,如果我們把非緩沖通道示例代碼中主協(xié)程方法中的第一行代碼解開注釋再運行,會發(fā)現程序出現了報錯。
表示所有的協(xié)程都睡眠了,發(fā)生了死鎖。
fatal error: all goroutines are asleep - deadlock!
其實原因也很簡單,就比如那上面的代碼來說。我們上來就在主協(xié)程中接收通道中的值,而此時通道中沒有值,所以主協(xié)程會阻塞,然而此時我們并沒有開啟其他的協(xié)程,那就相當于這個程序后面將不會有沒有任何操作,永遠鎖在了這個位置。所以如果在阻塞期間發(fā)現沒有正在執(zhí)行的協(xié)程,程序將爆出異常退出。
所以我們在對通道進行操作時,要注意千萬不要在邏輯上把自己鎖死了。
使用通道實現協(xié)程等待
用起來有點像 JAVA 中的 CountDownLatch
- 首先我們需要知道子協(xié)程的數量 num
- 然后我們創(chuàng)建一個大小為 num 的通道
- 當子協(xié)程完全執(zhí)行完之后就向通道中放一個元素值
- 主協(xié)程從通道中循環(huán)取值,取值的次數就是 num,取不到值時會阻塞,而這 num 個值只有當全部子協(xié)程都執(zhí)行完畢之后才能提供全,所以這樣就實現了主協(xié)程等待子協(xié)程
至于為什么要創(chuàng)建 struct{} 類型的通道,是因為空結構體占用了0字節(jié)的內存空間
func main() {
num := 5
sign := make(chan struct{}, num)
for i := 0; i < num; i++ {
go func() {
fmt.Println(i)
sign <- struct{}{}
}()
}
for j := 0; j < num; j++ {
<-sign
}
}到此這篇關于Golang控制通道實現協(xié)程等待詳解的文章就介紹到這了,更多相關Go協(xié)程等待內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
go-zero使用goctl生成mongodb的操作使用方法
mongodb是一種高性能、開源、文檔型的nosql數據庫,被廣泛應用于web應用、大數據以及云計算領域,goctl model 為 goctl 提供的數據庫模型代碼生成指令,目前支持 MySQL、PostgreSQL、Mongo 的代碼生成,本文給大家介紹了go-zero使用goctl生成mongodb的操作使用方法2024-06-06

