今天呢,咱們來聊聊 Go 語言的那點事兒,尤其是咱們在并發處理中常用的 select 語句,它可是處理并發時的一把利劍!
Go 語言的 select 語句,仿佛是編程世界中的一位冷靜的裁判,當多個通道(channel)全都爭著搶話語權的時候,它就會站出來,公平地判決誰應當先發聲。
換句話說,select 可以在多個通道之間等待并選擇可用的通道執行操作。
你得這么看select語句——它是并發編程領域里的一塊重要的拼圖,沒有這塊,你畫出的并發圖景就不完整。
首先,我們來看一個簡單的示例:
select {case <-chan1: // 操作1case data := <-chan2: // 操作2case chan3 <- data: // 操作3default: // 默認操作}
還別說,這幾行代碼,簡單明了,但它背后可是隱藏著深邃的并發處理智慧:
優雅!這是使用過 select 語句后,我心中的感嘆。就像你有了一塊功能強大的瑞士軍刀,可以靈活地應對各種野外求生的情況。
在代碼中,select 語句也可以靈活地處理多個通道的并發操作,避免使用復雜的同步工具實現并發操作。
講科技,不能光有干巴巴的代碼堆砌,還得有歷史沉淀(反正以前歷史老師是這么教的 :)。
而我們現在探討的是 Go 語言里的 select 思想,它最初源自于網絡 IO 模型中的 select,其精華在于 IO 多路復用。
想象一下,有那么一刻,你需要同時傾聽來自世界各地的廣播,這可不是一件簡單的事兒。然而,這正是 go 中的 channel 和 select 的日常所在:致力于協調多個渠道的信息流,也只有在這里,才有 “通道爭鳴” 的景象。
讓我們像切洋蔥一樣,一層層地剝開 select 神秘的外衣:
接下來,咱們通過一系列實驗來檢驗真實世界中 select 的行為。
(1) select 已關閉通道和空通道場景
再來看以下代碼:
func main() { c1, c2, c3 := make(chan bool), make(chan bool), make(chan bool) go func() { for { select { // 保證c1一定不會關閉,否則會死循環此case case <-c1: fmt.Println("case c1") // c2可以防止出現c1死循環的情況 // 如果c2關閉了(ok==false),將其置為nil,下次就會忽略此case case _, ok := <-c2: if !ok { fmt.Println("c2 is closed") c2 = nil } // 如果c3已關閉,則panic(不能往已關閉的channel里寫數據) // 如果c3為nil,則ignore該case case c3 <- true: fmt.Println("case c3") case v <- c4: fmt.Println(v) } } }() time.Sleep(10 * time.Second)}
當 channel 關閉以后,case <- chan 會接收該通信對應數據類型的零值,所以會出現死循環。
(2) 帶 default 語句實現非阻塞讀寫
select { case <- c1: fmt.Println(":case c3") // 當c1沒有消息時,不會一直阻塞,而是進入default default: fmt.Println(":select default")}
注意,Go 語言的 select 和 Java 或者 C 語言的 switch 還不太一樣:switch 中一般會帶有 default 判斷分支,但 select 使用時,外層的 for 循環和 default 不會同時出現,否則會發生死鎖。
(3) select 實現定時任務
func main() { done := make(chan bool) var selectTest = func() { for { select { case <-time.After(1 * time.Second): fmt.Println("Working...") case <-done: fmt.Println("Job done!") } } } go selectTest() time.Sleep(3 * time.Second) done <- true time.Sleep(500 * time.Microsecond)}
這個例子模擬的是一個簡易的定時器,每隔一秒鐘它都會打印 "Working..." 直到我們通過關閉 done 通道告訴它 "任務完成"。
這樣的模式在你需要定時檢查或者定時執行一些任務時非常有用!
代碼運行結果:
Working...
Working...
Job done!
注意,如果定時器的另外 case 分支是上面已關閉 channel 場景,可能會出現異常,如下所示:
func main() { done := make(chan bool) t := time.Now() var selectTest = func() { for { select { case <-time.After(100 * time.Microsecond): fmt.Println(time.Since(t), " time.After exec, return!") return case <-done: fmt.Println("over") } }} // 關閉 chan close(done) go selectTest() time.Sleep(2 * time.Second)}
我們在并發執行之前就 close(done) 關閉了 Channel,不妨猜一下這段代碼會輸出什么,答案是:
...
over
over
over
601.3938ms time.After exec, return!
這是因為:done 已經被關閉了,所以當執行 case <-done 語句時會死循環此 case 分支。
但是,為什么還會執行退出 case,而且 return 時,時間來到了 601.3938ms 呢?
從上面代碼中定時器 case 100 ms 執行一次,我們不難得知,程序退出時是第 6 次執行 select 語句,這里面究竟有什么魔法呢?
讓我們接著往下看!
上文已經描述過,如果多個 case 滿足讀取條件時,select 會隨機選擇一個語句執行。
讓我們用代碼來詳細描述一下:
func main() { done := make(chan int, 1024) tick := time.NewTicker(time.Second) var selectTest = func() { for i := 0; i < 10; i++ { select { case done <- i: fmt.Println(i, ", done exec") case <- tick.C: fmt.Println(i, ", time.After exec") } time.Sleep(500 * time.Millisecond) } } go selectTest() time.Sleep(5 * time.Second)}
這個例子開啟了一個 goroutine 協程來運行 selectTest 函數,在函數里面 for 循環 10 次執行 select 語句。并且,select 的兩個分支 case done <- i 和 case <- tick.C 都是可以執行的。
這時候,我們看一下執行結果:
0 , done exec
1 , done exec
2 , time.After exec
3 , done exec
4 , time.After exec
5 , done exec
6 , done exec
7 , done exec
8 , time.After exec
9 , done exec
注意,以上結果多次運行的打印順序可能不一致,是正常現象!
我們可以發現,原本寫入 done 通道的 2、4 和 8 不見了,說明在循環的過程中,select 的兩個分支 case done <- i 和 case <- tick.C 都是執行了的。
因此,這就驗證了當多個 case 同時滿足時,select 會隨機選擇一個執行。這個設計是為了避免某個 case 出現饑餓問題,保證公平競爭而引入的。
試想一下,如果某個 case 一直執行,而某些 case 一直得不到執行,這和 select 公平選擇的初衷就沖突了。
所以,Go 在十多年前新增 select 提交時就用了這種隨機策略并保留至今,雖然中途有過細微的變更,但整體語義一直沒有變化。
Go語言中 select用于處理多個通道(channel)的發送和接收操作,但在 Go 語言的源代碼中沒有直接對應的結構體。
因此,select通過runtime.scase結構體表示其中的每個 case,該結構體包含指向通道和數據元素的指針:
type scase struct {c *hchan // chanelem unsafe.Pointer // data element}
編譯時,select 語句被轉換成 OSELECT 節點,持有 OCASE 節點集合,每個 OCASE 代表一個可能的操作,包括空(對應 default)。
根據情況不同,編譯器會優化select的處理過程。優化處理的情況分為:
非阻塞操作進行相應的編譯器重寫,發送使用 runtime.selectnbsend 函數進行非阻塞發送,接收方面有兩種函數處理單值和雙值接收。
運行時,runtime.selectgo函數通過以下幾個步驟處理 select:
本文中,我們談到了 Go 語言里 select 的基本特性和實現,提到了select與直接 Channel 操作的相似性,以及通過 default 支持非阻塞收發操作。
我們還揭示了select 底層實現的復雜性——需要編譯器和運行時支持。
通過以上不難得知,Go 的 select 語句在不同場景下的行為和實現是比較奇妙的,這也是 Go 獨特的數據結構,其背后的設計與優化策略都需要我們對 Go 底層有著比較完善的認知。
本文鏈接:http://www.www897cc.com/showinfo-26-50032-0.html一文搞懂Go中select的隨機公平策略:并發編程的黃金法則
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com