Golang 的 select 機制可以理解為是在語言層面實現了和select, poll, epoll 相似的功能:監聽多個描述符的讀/寫等事件,一旦某個描述符就緒(一般是讀或者寫事件發生了),就能夠將發生的事件通知給關心的應用程序去處理該事件。 golang 的 select 機制是,監聽多個channel,每一個 case 是一個事件,可以是讀事件也可以是寫事件,隨機選擇一個執行,可以設置default.
它的作用是:當監聽的多個事件都阻塞住會執行default的邏輯。
select的源碼在 (runtime/select.go)\[[https://github.com/golang/go/blob/master/src/runtime/select.go](https://github.com/golang/go/blob/master/src/runtime/select.go)\] ,看的時候建議是重點關注 pollorder 和 lockorder.
* pollorder保存的是scase的序號,亂序是為了之后執行時的隨機性。
* lockorder保存了所有case中channel的地址,這里按照地址大小堆排了一下lockorder對應的這片連續內存。對chan排序是為了去重,保證之后對所有channel上鎖時不會重復上鎖。
goroutine作為Golang并發的核心,我們不僅要關注它們的創建和管理,當然還要關注如何合理的退出這些協程,不(合理)退出不然可能會造成阻塞、panic、程序行為異常、數據結果不正確等問題。goroutine在退出方面,不像線程和進程,不能通過某種手段強制關閉它們,只能等待goroutine主動退出。
goroutine的優雅退出方法有三種:
1. 使用for-range退出
for-range是使用頻率很高的結構,常用它來遍歷數據,range能夠感知channel的關閉,當channel被發送數據的協程關閉時,range就會結束,接著退出for循環。
它在并發中的使用場景是:當協程只從1個channel讀取數據,然后進行處理,處理后協程退出。下面這個示例程序,當in通道被關閉時,協程可自動退出。
~~~go
go func(in <-chan int) {
// Using for-range to exit goroutine
// range has the ability to detect the close/end of a channel
for x := range in {
fmt.Printf("Process %d\n", x)
}
}(in)
~~~
2. 使用select case ,ok退出
for-select也是使用頻率很高的結構,select提供了多路復用的能力,所以for-select可以讓函數具有持續多路處理多個channel的能力。但select沒有感知channel的關閉,這引出了2個問題:
繼續在關閉的通道上讀,會讀到通道傳輸數據類型的零值,如果是指針類型,讀到nil,繼續處理還會產生nil。 繼續在關閉的通道上寫,將會panic。
問題2可以這樣解決,通道只由發送方關閉,接收方不可關閉,即某個寫通道只由使用該select的協程關閉,select中就不存在繼續在關閉的通道上寫數據的問題。
問題1可以使用,ok來檢測通道的關閉,使用情況有2種。
第一種:如果某個通道關閉后,需要退出協程,直接return即可。示例代碼中,該協程需要從in通道讀數據,還需要定時打印已經處理的數量,有2件事要做,所有不能使用for-range,需要使用for-select,當in關閉時,ok=false,我們直接返回。
~~~go
go func() {
// in for-select using ok to exit goroutine
for {
select {
case x, ok := <-in:
if !ok {
return
}
fmt.Printf("Process %d\n", x)
processedCnt++
case <-t.C:
fmt.Printf("Working, processedCnt = %d\n", processedCnt)
}
}
}()
~~~
第二種:如果某個通道關閉了,不再處理該通道,而是繼續處理其他case,退出是等待所有的可讀通道關閉。我們需要使用select的一個特征:select不會在nil的通道上進行等待。這種情況,把只讀通道設置為nil即可解決。
~~~go
go func() {
// in for-select using ok to exit goroutine
for {
select {
case x, ok := <-in1:
if !ok {
in1 = nil
}
// Process
case y, ok := <-in2:
if !ok {
in2 = nil
}
// Process
case <-t.C:
fmt.Printf("Working, processedCnt = %d\n", processedCnt)
}
// If both in channel are closed, goroutine exit
if in1 == nil && in2 == nil {
return
}
}
}()
~~~
3. 使用退出通道退出
使用,ok來退出使用for-select協程,解決是當讀入數據的通道關閉時,沒數據讀時程序的正常結束。想想下面這2種場景,,ok還能適用嗎?
接收的協程要退出了,如果它直接退出,不告知發送協程,發送協程將阻塞。啟動了一個工作協程處理數據,如何通知它退出?
使用一個專門的通道,發送退出的信號,可以解決這類問題。以第2個場景為例,協程入參包含一個停止通道stopCh,當stopCh被關閉,case <-stopCh會執行,直接返回即可。
當我啟動了100個worker時,只要main()執行關閉stopCh,每一個worker都會都到信號,進而關閉。如果main()向stopCh發送100個數據,這種就低效了。
~~~go
func worker(stopCh <-chan struct{}) {
go func() {
defer fmt.Println("worker exit")
// Using stop channel explicit exit
for {
select {
case <-stopCh:
fmt.Println("Recv stop signal")
return
case <-t.C:
fmt.Println("Working .")
}
}
}()
return
}
~~~
通過channel控制子goroutine的方法可以總結為:循環監聽一個channel,一般來說是for循環里放一個select監聽channel以達到通知子goroutine的效果。再借助Waitgroup,主進程可以等待所有協程優雅退出后再結束自己的運行,這就通過channel實現了優雅控制goroutine并發的開始和結束。
因此在退出協程的時候需要注意:
* 發送協程主動關閉通道,接收協程不關閉通道。使用技巧:把接收方的通道入參聲明為只讀,如果接收協程關閉只讀協程,編譯時就會報錯。
* 協程處理1個通道,并且是讀時,協程優先使用for-range,因為range可以關閉通道的關閉自動退出協程。
* ok可以處理多個讀通道關閉,需要關閉當前使用for-select的協程。
* 顯式關閉通道stopCh可以處理主動通知協程退出的場景。
- Golang基礎
- Go中new與make的區別
- Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量
- 無緩沖Chan的發送和接收是否同步
- Golang并發機制以及它所使用的CSP并發模型.
- Golang中常用的并發模型
- Go中對nil的Slice和空Slice的處理是一致的嗎
- 協程和線程和進程的區別
- Golang的內存模型中為什么小對象多了會造成GC壓力
- Go中數據競爭問題怎么解決
- 什么是channel,為什么它可以做到線程安全
- Golang垃圾回收算法
- GC的觸發條件
- Go的GPM如何調度
- 并發編程概念是什么
- Go語言的棧空間管理是怎么樣的
- Goroutine和Channel的作用分別是什么
- 怎么查看Goroutine的數量
- Go中的鎖有哪些
- 怎么限制Goroutine的數量
- Channel是同步的還是異步的
- Goroutine和線程的區別
- Go的Struct能不能比較
- Go的defer原理是什么
- Go的select可以用于什么
- Context包的用途是什么
- Go主協程如何等其余協程完再操作
- Go的Slice如何擴容
- Go中的map如何實現順序讀取
- Go中CAS是怎么回事
- Go中的逃逸分析是什么
- Go值接收者和指針接收者的區別
- Go的對象在內存中是怎樣分配的
- 棧的內存是怎么分配的
- 堆內存管理怎么分配的
- 在Go函數中為什么會發生內存泄露
- G0的作用
- Go中的鎖如何實現
- Go中的channel的實現
- 棧的內存是怎么分配的2
- 堆內存管理怎么分配的2
- Go中的map的實現
- Go中的http包的實現原理
- Goroutine發生了泄漏如何檢測
- Go函數返回局部變量的指針是否安全
- Go中兩個Nil可能不相等嗎
- Goroutine和KernelThread之間是什么關系
- 為何GPM調度要有P
- 如何在goroutine執行一半就退出協程
- Mysql基礎
- Mysql索引用的是什么算法
- Mysql事務的基本要素
- Mysql的存儲引擎
- Mysql事務隔離級別
- Mysql高可用方案有哪些
- Mysql中utf8和utf8mb4區別
- Mysql中樂觀鎖和悲觀鎖區別
- Mysql索引主要是哪些
- Mysql聯合索引最左匹配原則
- 聚簇索引和非聚簇索引區別
- 如何查詢一個字段是否命中了索引
- Mysql中查詢數據什么情況下不會命中索引
- Mysql中的MVCC是什么
- Mvcc和Redolog和Undolog以及Binlog有什么不同
- Mysql讀寫分離以及主從同步
- InnoDB的關鍵特性
- Mysql如何保證一致性和持久性
- 為什么選擇B+樹作為索引結構
- InnoDB的行鎖模式
- 哈希(hash)比樹(tree)更快,索引結構為什么要設計成樹型
- 為什么索引的key長度不能太長
- Mysql的數據如何恢復到任意時間點
- Mysql為什么加了索引可以加快查詢
- Explain命令有什么用
- Redis基礎
- Redis的數據結構及使用場景
- Redis持久化的幾種方式
- Redis的LRU具體實現
- 單線程的Redis為什么快
- Redis的數據過期策略
- 如何解決Redis緩存雪崩問題
- 如何解決Redis緩存穿透問題
- Redis并發競爭key如何解決
- Redis的主從模式和哨兵模式和集群模式區別
- Redis有序集合zset底層怎么實現的
- 跳表的查詢過程是怎么樣的,查詢和插入的時間復雜度
- 網絡協議基礎
- TCP和UDP有什么區別
- TCP中三次握手和四次揮手
- TCP的LISTEN狀態是什么
- 常見的HTTP狀態碼有哪些
- 301和302有什么區別
- 504和500有什么區別
- HTTPS和HTTP有什么區別
- Quic有什么優點相比Http2
- Grpc的優缺點
- Get和Post區別
- Unicode和ASCII以及Utf8的區別
- Cookie與Session異同
- Client如何實現長連接
- Http1和Http2和Grpc之間的區別是什么
- Tcp中的拆包和粘包是怎么回事
- TFO的原理是什么
- TIME_WAIT的作用
- 網絡的性能指標有哪些