一看就懂系列之Golang的goroutine和通道
版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/u011957758/article/details/81159481
https://blog.csdn.net/u011957758/article/details/81159481
前言
如果說php是最好的語言,那麼golang就是最併發的語言。
支援golang的併發很重要的一個是goroutine的實現,那麼本文將重點圍繞goroutine來做一下相關的筆記,以便日後快速留戀。
10s後,以下知識點即將靠近:
1.從併發模型說起
2.goroutine的簡介
3.goroutine的使用姿勢
4.通道(channel)的簡介
5.重要的四種通道使用
6.goroutine死鎖與處理
7.select的簡介
8.select的應用場景
9.select死鎖
正文
1.從併發模型說起
看過很多大神簡介,各種研究高併發,那麼就通俗的說下併發。
併發目前來看比較主流的就三種:
1.多執行緒
每個執行緒一次處理一個請求,執行緒越多可併發處理的請求數就越多,但是在高併發下,多執行緒開銷會比較大。
2.協程
無需搶佔式的排程,開銷小,可以有效的提高執行緒的併發性,從而避免了執行緒的缺點的部分
3.基於非同步回撥的IO模型
說一個熟悉的,比如nginx使用的就是epoll模型,通過事件驅動的方式與非同步IO回撥,使得伺服器持續運轉,來支撐高併發的請求
為了追求更高效和低開銷的併發,golang的goroutine來了。
2.goroutine的簡介
定義:在go裡面,每一個併發執行的活動成為goroutine。
詳解:goroutine可以認為是輕量級的 執行緒 ,與建立執行緒相比,建立 成本和開銷都很小 ,每個goroutine的堆疊只有 幾kb ,並且堆疊可根據程式的需要增長和縮小(執行緒的堆疊需指明和固定),所以go程式從語言層面支援了高併發。
程式執行的背後:當一個程式啟動的時候,只有一個goroutine來呼叫main函式,稱它為主goroutine,新的goroutine通過go語句進行建立。
3.goroutine的使用姿勢
3.1單個goroutine建立
在函式或者方法前面加上關鍵字go,即建立一個併發執行的新goroutine。
上程式碼:
package main import ( "fmt" "time" ) func HelloWorld() { fmt.Println("Hello world goroutine") } func main() { go HelloWorld() // 開啟一個新的併發執行 time.Sleep(1*time.Second) fmt.Println("我後面才輸出來") }
以上執行後會輸出:
Hello world goroutine 我後面才輸出來
需要注意的是,執行速度很快,一定要加sleep,不然你一定可以看到goroutine裡頭的輸出。
這也說明了一個關鍵點: 當main函式返回時,所有的gourutine都是暴力終結的,然後程式退出。
3.2多個goroutine建立
package main import ( "fmt" "time" ) func DelayPrint() { for i := 1; i <= 4; i++ { time.Sleep(250 * time.Millisecond) fmt.Println(i) } } func HelloWorld() { fmt.Println("Hello world goroutine") } func main() { go DelayPrint() // 開啟第一個goroutine go HelloWorld() // 開啟第二個goroutine time.Sleep(2*time.Second) fmt.Println("main function") }
函式輸出:
Hello world goroutine 1 2 3 4 5 main function
有心的同學可能會發現,DelayPrint裡頭有sleep,那麼會導致第二個goroutine堵塞或者等待嗎?
答案是:no
疑惑: 當程式執行go FUNC()的時候,只是簡單的呼叫然後就立即返回了,並不關心函式裡頭髮生的故事情節,所以不同的goroutine直接不影響,main會繼續按順序執行語句。
4.通道(channel)的簡介
4.1簡介
如果說goroutine是Go併發的執行體,那麼”通道”就是他們之間的連線。
通道可以讓一個goroutine傳送特定的值到另外一個goroutine的通訊機制。
4.2宣告&傳值&關閉
宣告
var ch chan int // 宣告一個傳遞int型別的channel ch := make(chan int) // 使用內建函式make()定義一個channel //========= ch <- value // 將一個數據value寫入至channel,這會導致阻塞,直到有其他goroutine從這個channel中讀取資料 value := <-ch // 從channel中讀取資料,如果channel之前沒有寫入資料,也會導致阻塞,直到channel中被寫入資料為止 //========= close(ch) // 關閉channel
有沒注意到關鍵字” 阻塞 “?,這個其實是預設的channel的接收和傳送,其實也有非阻塞的,請看下文。
5.重要的四種通道使用
1.無緩衝通道
說明:無緩衝通道上的傳送操作將會被阻塞,直到另一個goroutine在對應的通道上執行接收操作,此時值才傳送完成,兩個goroutine都繼續執行。
上程式碼:
package main import ( "fmt" "time" ) var done chan bool func HelloWorld() { fmt.Println("Hello world goroutine") time.Sleep(1*time.Second) done <- true } func main() { done = make(chan bool) // 建立一個channel go HelloWorld() <-done }
輸出:
Hello world goroutine
由於main不會等goroutine執行結束才返回,前文專門加了sleep輸出為了可以看到goroutine的輸出內容,那麼在這裡由於是 阻塞 的,所以無需sleep。
(小嚐試:可以將程式碼中”done <- true”和”<-done”,去掉再執行,看看會發生啥?)
2.管道
通道可以用來連線goroutine,這樣一個的輸出是另一個輸入。這就叫做管道。
例子:
package main import ( "fmt" "time" ) var echo chan string var receive chan string // 定義goroutine 1 func Echo() { time.Sleep(1*time.Second) echo <- "咖啡色的羊駝" } // 定義goroutine 2 func Receive() { temp := <- echo // 阻塞等待echo的通道的返回 receive <- temp } func main() { echo = make(chan string) receive = make(chan string) go Echo() go Receive() getStr := <-receive // 接收goroutine 2的返回 fmt.Println(getStr) }
在這裡不一定要去關閉channel,因為底層的垃圾回收機制會根據它 是否可以訪問來決定是否自動回收它 。(這裡不是根據channel是否關閉來決定的)
3.單向通道型別
當程式則夠複雜的時候,為了程式碼可讀性更高,拆分成一個一個的小函式是需要的。
此時go提供了單向通道的型別,來實現函式之間channel的傳遞。
上程式碼:
package main import ( "fmt" "time" ) // 定義goroutine 1 func Echo(out chan<- string) { // 定義輸出通道型別 time.Sleep(1*time.Second) out <- "咖啡色的羊駝" close(out) } // 定義goroutine 2 func Receive(out chan<- string, in <-chan string) { // 定義輸出通道型別和輸入型別 temp := <-in // 阻塞等待echo的通道的返回 out <- temp close(out) } func main() { echo := make(chan string) receive := make(chan string) go Echo(echo) go Receive(receive, echo) getStr := <-receive // 接收goroutine 2的返回 fmt.Println(getStr) }
程式輸出:
咖啡色的羊駝
4.緩衝管道
goroutine的通道預設是是阻塞的,那麼有什麼辦法可以緩解阻塞?
答案是:加一個緩衝區。
對於go來說建立一個緩衝通道很簡單:
ch := make(chan string, 3) // 建立了緩衝區為3的通道 //========= len(ch) // 長度計算 cap(ch) // 容量計算
6.goroutine死鎖與友好退出
6.1goroutine死鎖
來一個死鎖現場一:
package main func main() { ch := make(chan int) <- ch // 阻塞main goroutine, 通道被鎖 }
輸出:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: main.main()
死鎖現場2:
package main func main() { cha, chb := make(chan int), make(chan int) go func() { cha <- 1 // cha通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine chb <- 0 }() <- chb // chb 等待資料的寫 }
為什麼會有死鎖的產生?
非緩衝通道上如果發生了流入無流出,或者流出無流入,就會引起死鎖。
或者這麼說:goroutine的非緩衝通道里頭一定要一進一出,成對出現才行。
上面例子屬於:一:流出無流入;二:流入無流出
當然,有一個例外:
func main() { ch := make(chan int) go func() { ch <- 1 }() }
執行以上程式碼將會發現,竟然沒有報錯。
what?不是說好的一進一出就死鎖嗎?
仔細研究會發現,其實根本沒等goroutine執行完,main函式自己先跑完了,所以就沒有資料流入主的goroutine,就不會被阻塞和報錯
6.2goroutine的死鎖處理
有兩種辦法可以解決:
1.把沒取走的取走便是
package main func main() { cha, chb := make(chan int), make(chan int) go func() { cha <- 1 // cha通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine chb <- 0 }() <- cha // 取走便是 <- chb // chb 等待資料的寫 }
2.建立緩衝通道
package main func main() { cha, chb := make(chan int, 3), make(chan int) go func() { cha <- 1 // cha通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine chb <- 0 }() <- chb // chb 等待資料的寫 }
這樣的話,cha可以快取一個數據,cha就不會掛起當前的goroutine了。除非再放兩個進去,塞滿緩衝通道就會了。
7.select的簡介
定義:在golang裡頭select的功能與epoll(nginx)/poll/select的功能類似,都是堅挺IO操作,當IO操作發生的時候,觸發相應的動作。
select有幾個重要的點要強調:
1.如果有多個case都可以執行,select會隨機公平地選出一個執行,其他不會執行
上程式碼:
package main import "fmt" func main() { ch := make (chan int, 1) ch<-1 select { case <-ch: fmt.Println("咖啡色的羊駝") case <-ch: fmt.Println("黃色的羊駝") } }
輸出:
(隨機)二者其一
2.case後面必須是channel操作,否則報錯。
上程式碼:
package main import "fmt" func main() { ch := make (chan int, 1) ch<-1 select { case <-ch: fmt.Println("咖啡色的羊駝") case 2: fmt.Println("黃色的羊駝") } }
輸出報錯:
2 evaluated but not used select case must be receive, send or assign recv
3.select中的default子句總是可執行的。所以沒有default的select才會阻塞等待事件
上程式碼:
package main import "fmt" func main() { ch := make (chan int, 1) // ch<-1 <= 注意這裡備註了。 select { case <-ch: fmt.Println("咖啡色的羊駝") default: fmt.Println("黃色的羊駝") } }
輸出:
黃色的羊駝
4.沒有執行的case,那麼江湖阻塞事件發生報錯(死鎖)
package main import "fmt" func main() { ch := make (chan int, 1) // ch<-1 <= 注意這裡備註了。 select { case <-ch: fmt.Println("咖啡色的羊駝") } }
輸出報錯:
fatal error: all goroutines are asleep - deadlock!
8.select的應用場景
1.timeout 機制(超時判斷)
package main import ( "fmt" "time" ) func main() { timeout := make (chan bool, 1) go func() { time.Sleep(1*time.Second) // 休眠1s,如果超過1s還沒I操作則認為超時,通知select已經超時啦~ timeout <- true }() ch := make (chan int) select { case <- ch: case <- timeout: fmt.Println("超時啦!") } }
以上是入門版,通常程式碼中是這麼寫的:
package main import ( "fmt" "time" ) func main() { ch := make (chan int) select { case <-ch: case <-time.After(time.Second * 1): // 利用time來實現,After代表多少時間後執行輸出東西 fmt.Println("超時啦!") } }
2.判斷channel是否阻塞(或者說channel是否已經滿了)
package main import ( "fmt" ) func main() { ch := make (chan int, 1) // 注意這裡給的容量是1 ch <- 1 select { case ch <- 2: default: fmt.Println("通道channel已經滿啦,塞不下東西了!") } }
3.退出機制
package main import ( "fmt" "time" ) func main() { i := 0 ch := make(chan string, 0) defer func() { close(ch) }() go func() { DONE: for { time.Sleep(1*time.Second) fmt.Println(time.Now().Unix()) i++ select { case m := <-ch: println(m) break DONE // 跳出 select 和 for 迴圈 default: } } }() time.Sleep(time.Second * 4) ch<-"stop" }
輸出:
1532390471 1532390472 1532390473 stop 1532390474
這邊要強調一點:退出迴圈一定要用break + 具體的標記,或者goto也可以。否則其實不是真的退出。
package main import ( "fmt" "time" ) func main() { i := 0 ch := make(chan string, 0) defer func() { close(ch) }() go func() { for { time.Sleep(1*time.Second) fmt.Println(time.Now().Unix()) i++ select { case m := <-ch: println(m) goto DONE // 跳出 select 和 for 迴圈 default: } } DONE: }() time.Sleep(time.Second * 4) ch<-"stop" }
輸出:
1532390525 1532390526 1532390527 1532390528 stop
9.select死鎖
select不注意也會發生死鎖,前文有提到一個,這裡分幾種情況,重點再次強調:
1.如果沒有資料需要傳送,select中又存在接收通道資料的語句,那麼將傳送死鎖
package main func main() { ch := make(chan string) select { case <-ch: } }
預防的話加default。
空select,也會引起死鎖
package main func main() { select {} }
- 優維低程式碼:Route Alias 路由別名和Segues 頁面切換
- Go語言愛好者週刊:第 161 期
- 手寫程式語言-實現運算子過載
- Go語言愛好者週刊:第 160 期 — 竟然這麼多人不理解 map 的 make 含義
- 低程式碼實戰 | 1分鐘,從0到1建立一個簡單的微應用
- 優維低程式碼:Pipes 管道
- 【1-2 Golang】Go語言快速入門—陣列與切片
- 里程碑!用自己的程式語言實現了一個網站
- 【1-1 Golang】Go語言快速入門—基本語法
- Go語言愛好者週刊:第 159 期 — 這道題目有點意思
- 萬字長文告訴你Go 1.19中值得關注的幾個變化
- 9月更新!7個超好用的功能上線了!EasyOps®UI8.0更有大變動
- 碼住!Golang併發安全與引用傳遞總結
- Google雲基礎架構工程師:視覺隱喻的混沌工程和可觀察性
- 統一的可觀察性:指標、日誌和跟蹤
- 用位運算為你的程式加速
- 如何用Golang來手擼一個Blog - Milu.blog 開發總結
- 十七年運維老兵萬字長文講透優維低程式碼~
- 一文讀懂 Kubernetes的四種服務型別!
- 優維低程式碼:Use Resolves