Golang實現(xiàn)帶優(yōu)先級的select
背景
在 Golang 里面,我們經(jīng)常使用 channel 進行協(xié)程之間的通信。這里有一個經(jīng)典的場景,也就是生產(chǎn)者消費者模式,生產(chǎn)者協(xié)程不斷地往 Channel 里面塞元素,而消費者協(xié)程不斷地消費這些元素。
寫成代碼就是如下:
package main import ( "fmt" "sync" "time" ) func main() { ch := make(chan int, 10) var wg sync.WaitGroup wg.Add(2) go producer(ch, &wg) go consumer(ch, &wg) wg.Wait() } // 生產(chǎn)者 func producer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() i := 0 for { select { case ch <- i: default: // 丟棄 log.Println("discard") } i++ time.Sleep(time.Second) } } // 消費者 func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() consume := func(i int) { fmt.Println(i) time.Sleep(time.Millisecond * 700) } for { i := <-ch consume(i) // 消費元素 } }
生產(chǎn)者不斷產(chǎn)生元素,消費者消費元素。生產(chǎn)者不會等待消費者消費完畢(不然可能影響其他任務),如果 channel 已經(jīng)滿了,也就是說明消費者消費不過來,生產(chǎn)者就會丟棄這個任務。
生產(chǎn)者平均一秒生成1個,消費者0.7秒消費一個。正常情況下消費者是消費得過來的,然而很多時候消費者協(xié)程還需要做一些定時任務,比如一些定時清理工作。假如這個清理工作每2秒觸發(fā)一次,清理時間一般需要1.5秒,也就是如果每次都做每一秒有0.75秒會被清理工作占有了,但是它不是一定要非常及時的,可以等空閑時再進行。 如下代碼:
// 消費者 func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() t := time.NewTicker(time.Second * 2) consume := func(i int) { fmt.Println(i) time.Sleep(time.Millisecond * 700) } clear := func() { fmt.Println("clear") time.Sleep(time.Millisecond * 1500) } for { select { case i := <-ch: consume(i) // 消費元素: case <-t.C: clear() // 清理 } } }
運行程序到第15秒的時候,生產(chǎn)者發(fā)現(xiàn) channel滿了,于是開始丟包:
0
1
clear
2
3
4
5
6
clear
7
clear
8
clear
9
clear
clear
10
clear
11
12
13
14
clear
15
clear
clear
discard
16
clear
discard
discard
解決方案
既然清理任務的優(yōu)先級并不高,那么它就不應該阻塞消費元素流程,而是應該在空閑時才去執(zhí)行。由于 Golang 里面,如果 select 兩個 case 都同時滿足,會隨機選一個執(zhí)行,因此第一想到的可能會使用如下代碼實現(xiàn)優(yōu)先級case:
// 消費者 func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() t := time.NewTicker(time.Second * 2) consume := func(i int) { fmt.Println(i) time.Sleep(time.Millisecond * 700) } clear := func() { fmt.Println("clear") time.Sleep(time.Millisecond * 1500) } for { select { case i := <-ch: consume(i) // 消費元素 continue // 可能還有元素,不走清理邏輯 default: } // 沒有元素才走清理邏輯 select { case <-t.C: clear() // 清理 default: } } }
如果運行這個程序,可以發(fā)現(xiàn)它能夠滿足優(yōu)先級的需求,先消費元素,空閑時再執(zhí)行清理任務。
然而,在沒有元素可以消費,也沒有清理任務可以執(zhí)行的時候,這里的for將會不斷地循環(huán),浪費CPU資源。
其實,可以使用下面的方法實現(xiàn)優(yōu)先級case,它能夠在沒有元素就緒的時候阻塞在 select,而不是不斷循環(huán):
// 消費者 func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() t := time.NewTicker(time.Second * 2) consume := func(i int) { fmt.Println(i) time.Sleep(time.Millisecond * 700) } clear := func() { fmt.Println("clear") time.Sleep(time.Millisecond * 1500) } for { select { case i := <-ch: consume(i) // 消費元素 case <-t.C: priority: for { // 清理前先把元素消費完 select { case i := <-ch: consume(i) // 消費元素 default: break priority // 注:這里會跳過這個循環(huán),而不是再次執(zhí)行 } } clear() // 清理 } } }
這里的關(guān)鍵是在觸發(fā)清理case的時候,先去把channel里面的元素消費完,再進行清理,從而保證能夠留下足夠的channel緩沖區(qū)給生產(chǎn)者放置生產(chǎn)的元素。
一個封裝
上面那段優(yōu)先級case代碼其實挺常用的,但是幾乎都是模板代碼,特別是需要在兩個地方寫consume(i)
,因此我們可以封裝一下這段代碼,方便使用,減少出錯:
// 優(yōu)先級select ch1 的任務先執(zhí)行完畢后才會執(zhí)行 ch2 里面的任務 func PrioritySelect[T1, T2 any](ch1 <-chan T1, f1 func(T1), ch2 <-chan T2, f2 func(T2)) { for { select { case a := <-ch1: f1(a) case b := <-ch2: priority: for { select { case a := <-ch1: f1(a) default: break priority } } f2(b) } } }
這樣,我們的消費者代碼就可以簡化為:
// 消費者 func consumer(ch chan int, wg *sync.WaitGroup) { defer wg.Done() t := time.NewTicker(time.Second * 2) consume := func(i int) { fmt.Println(i) time.Sleep(time.Millisecond * 700) } clear := func(time.Time) { fmt.Println("clear") time.Sleep(time.Millisecond * 1500) } PrioritySelect(ch, consume, t.C, clear) }
到此這篇關(guān)于Golang實現(xiàn)帶優(yōu)先級的select的文章就介紹到這了,更多相關(guān)Golang帶優(yōu)先級select內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言中使用flag包對命令行進行參數(shù)解析的方法
這篇文章主要介紹了Go語言中使用flag包對命令行進行參數(shù)解析的方法,文中舉了一個實現(xiàn)flag.Value接口來自定義flag的例子,需要的朋友可以參考下2016-04-04Golang中的[]byte與16進制(String)之間的轉(zhuǎn)換方式
這篇文章主要介紹了Golang中的[]byte與16進制(String)之間的轉(zhuǎn)換方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11gin通過go build -tags實現(xiàn)json包切換及庫分析
這篇文章主要為大家介紹了gin通過go build -tags實現(xiàn)json包切換及庫分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09go讀取request.Body內(nèi)容踩坑實戰(zhàn)記錄
很多初學者在使用Go語言進行Web開發(fā)時,都會遇到讀取 request.Body內(nèi)容的問題,這篇文章主要給大家介紹了關(guān)于go讀取request.Body內(nèi)容踩坑實戰(zhàn)記錄的相關(guān)資料,需要的朋友可以參考下2023-11-11Golang 實現(xiàn)獲取當前函數(shù)名稱和文件行號等操作
這篇文章主要介紹了Golang 實現(xiàn)獲取當前函數(shù)名稱和文件行號等操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05