Go channel 的妙用
昨天在內網上看到一篇講資料庫連線的文章,列出了一些 sql 包的一些原始碼,我注意到其中取用、歸還連線的方式非常有意思——通過臨時建立的 channel 來傳遞連線。
在 sql.DB 結構體裡,使用 freeConn
欄位來表示當前所有的連線,也就是一個連線池。
type DB struct {
freeConn []*driverConn
}
當需要拿連線的時候,從 freeConn 中取出第一個元素:
conn := db.freeConn[0]
copy(db.freeConn, db.freeConn[1:])
db.freeConn = db.freeConn[:numFree-1]
conn.inUse = true
取 slice 切片的第一個元素,然後將 slice 後面的元素往前挪,最後通過截斷來 “釋放” 最後一個元素。
當然,能進行上述操作的前提是切片 db.freeConn
長度大於 0,即有空閒連線存在。如果當前沒有空閒連線,那如何處理呢?接下來就是 channel 的妙用的地方。
sql.DB
結構體裡還有另一個欄位 connRequests
,它用來儲存當前有哪些 “協程” 在申請連線:
type DB struct {
freeConn []*driverConn
connRequests map[uint64]chan connRequest
}
connRequests
的 key 是一個 uint64 型別,其實就是一個遞增加 1 的 key;而 connRequest
表示申請一個新連線的請求:
type connRequest struct {
conn *driverConn
err error
}
這裡的 conn
正是需要的連線。
當連線池中沒有空閒連線的時候:
req := make(chan connRequest, 1)
reqKey := db.nextRequestKeyLocked()
db.connRequests[reqKey] = req
先是構建了一個 chan connRequest
,同時拿到了一個 reqKey,將它和 req 繫結到 connRequests 中。
接下來,在 select 中等待超時或者從 req 這個 channel 中拿到空閒連線:
select {
case <-ctx.Done():
case ret, ok := <-req:
if !ok {
return nil, errDBClosed
}
return ret.conn, ret.err
}
可以看到,select 有兩個 case,第一個是通過 context 控制的 <-Done
;第二個則是前面構造的 <-req
,如果從 req 中讀出了元素,那就相當於獲得了連線:ret.conn。
那什麼時候會向 req 中傳送連線呢?答案是在向連線池歸還連線的時候。
前面提到,空閒連線是一個切片,歸還的時候直接 append 到這個切片就可以了:
func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
db.freeConn = append(db.freeConn, dc)
}
但其實在 append 之前,還會去檢查當前 connRequests 中是否有申請空閒連線的請求:
if c := len(db.connRequests); c > 0 {
var req chan connRequest
var reqKey uint64
for reqKey, req = range db.connRequests {
break
}
delete(db.connRequests, reqKey) // Remove from pending requests.
if err == nil {
dc.inUse = true
}
req <- connRequest{
conn: dc,
err: err,
}
return true
}
如果有請求的話,直接將當前連線 “塞到” req channel 裡去了。另一邊,申請連線的 goroutine 就可以從 req channel 中讀出 conn。
於是,通過 channel 就實現了一次 “連線傳輸” 的功能。
這讓我想到不久之前芮神寫的一篇《高併發服務遇 redis 瓶頸引發 time-wait 事故》,文中提到了將多個 redis command 組裝為一個 pipeline:
呼叫方把 redis command 和接收結果的 chan 推送到任務佇列中,然後由一個 worker 去消費,worker 組裝多個 redis cmd 為 pipeline,向 redis 發起請求並拿回結果,拆解結果集後,給每個命令對應的結果 chan 推送結果。呼叫方在推送任務到佇列後,就一直監聽傳輸結果的 chan。
這裡的用法就和本文描述的 channel 用法一致。
細想一下,以上提到的 channel 用法很神奇嗎?我們平時沒有接觸過嗎?
我用過最多的是 “生產者 - 消費者” 模式,先啟動 N 個 goroutine 消費者,讀某個 channel,之後,生產者再在某個時候向 channel 中傳送元素:
for i := 0; i < engine.workerNum; i++ {
go func() {
for {
work = <-engine.workChan
}
}
}
另外,我還會用 channel 充當一個 “ready” 的訊號,用來指示某個 “過程” 準備好了,可以接收結果了:
func (j *Job) Finished() <-chan bool {
return j.finish
}
前面提到的 “生產者 - 消費者” 和 “ready” 訊號這兩種 channel 用法和本文的 channel 用法並沒有什麼本質區別。唯一不同的點是前者的 channel 是事先建立好的,並且是 “公用” 的;而本文中用到的 channel 實際上是 “臨時” 建立的,並且只有這一個請求使用。
最後,用曹大最近在讀者群裡說的話結尾:
- 抄程式碼是很好的學習方式。
- 選一兩個感興趣的方向,自己嘗試實現相應的 feature list,實現完和標準實現做對比。
- 先積累再創造,別一上來就想著造輪子,看的多了碰上很多東西就有新思路了。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Go sync.Once 的妙用Go
- Go – Channel 原理Go
- go channel ->同步Go
- go : channel , queue , 程式管理 , 關閉channel ?Go
- go併發 - channelGo
- Go channel 介紹Go
- Go的Channel傳送和接收Go
- 圖解Go的channel底層原理圖解Go
- Go 中的 channel 怎麼實現的?Go
- Go實戰-基於Go協程和channel的使用Go
- 深度解密 Go 語言之 channel解密Go
- 深度解密Go語言之channel解密Go
- Go--關於 goroutine、channelGo
- Go channel 實現原理分析Go
- go channel學習筆記Go筆記
- Go Channel 詳細介紹Go
- 如何優雅的關閉Go Channel「譯」Go
- 使用 Go Channel 及 Goroutine 時機Go
- Go死鎖——當Channel遇上Mutex時GoMutex
- 【Go進階—資料結構】ChannelGo資料結構
- go 技巧: 實現一個無限 buffer 的 channelGo
- Go 中的 channel 與 Java BlockingQueue 的本質區別GoJavaBloC
- go嘗試從channel c接收資料,並檢查channel是否關閉Go
- Go基礎系列:雙層channel用法示例Go
- 十.Go併發程式設計--channel使用Go程式設計
- 用 Go 語言 buffered channel 實作 Job QueueGo
- ActionChains 的妙用AI
- reduce的妙用
- Go Quiz: 從Go面試題看channel在select場景下的注意事項GoUI面試題
- Channel(管道)- 《Go 專家程式設計》筆記提要Go程式設計筆記
- 一份儘可能全面的Go channel介紹Go
- 清華尹成帶你實戰GO案例(21)Go 並行通道ChannelGo並行
- go的協程及channel與web開發的一點小實踐GoWeb
- 通俗易懂剖析Go Channel:理解併發通訊的核心機制Go
- Linux:“awk”命令的妙用Linux
- IntegerCache的妙用和陷阱
- 二分的妙用
- 妙用ConstraintLayout的Circular positioningAI