Go 中的 channel 怎麼實現的?
相信大家在開發的過程中經常會使用到go
中併發利器channel
,channel
是CSP
併發模型中最重要的一個元件,兩個獨立的併發實體通過共享的通訊channel
進行通訊。大多數人只是會用這麼個結構很少有人討論它底層實現,這篇文章講寫寫channel
的底層實現。
channel
channel
的底層實現是一個結構體,原始碼如下:
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
可能看原始碼不是很好看得懂,這裡我個人畫了一張圖方便大家檢視,我在上面標註了不同顏色,並且註釋其作用。
通道像一個傳送帶
或者佇列
,總是遵循FIFO
的規則,保證收發資料的順序,通道是goroutine
間重要通訊的方式,是併發安全的。
buf
hchan
結構體中的buf
指向一個迴圈佇列,用來實現迴圈佇列,sendx
是迴圈佇列的隊尾指標,recvx
是迴圈佇列的隊頭指標,dataqsize
是快取型通道的大小,qcount
是記錄通道內元素個數。
在日常開發過程中用的最多就是ch := make(chan int, 10)
這樣的方式建立一個通道,如果這要宣告初始化的話,這個通道就是有緩衝區的,也是圖上紫色的buf
,buf
是在make
的時候程式建立的,它有元素大小*元素個數
組成一個迴圈佇列,可以看做成一個環形結構,buf
則是一個指標指向這個環。
上圖對應的程式碼那就是ch = make(chan int,6)
,buf
指向這個環在heap
上的地址。
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// buf points into the same allocation, elemtype is persistent.
// SudoG's are referenced from their owning thread so they can't be collected.
// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
var c *hchan
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
return c
}
上面就是對應的程式碼實現,上來它會檢查你一系列引數是否合法,然後在通過mallocgc
在記憶體開闢這塊空間,然後返回。
sendx & recvx
下面我手動模擬一個ring
實現的程式碼:
// Queue cycle buffer
type CycleQueue struct {
data []interface{} // 存放元素的陣列,準確來說是切片
frontIndex, rearIndex int // frontIndex 頭指標,rearIndex 尾指標
size int // circular 的大小
}
// NewQueue Circular Queue
func NewQueue(size int) (*CycleQueue, error) {
if size <= 0 || size < 10 {
return nil, fmt.Errorf("initialize circular queue size fail,%d not legal,size >= 10", size)
}
cq := new(CycleQueue)
cq.data = make([]interface{}, size)
cq.size = size
return cq, nil
}
// Push add data to queue
func (q *CycleQueue) Push(value interface{}) error {
if (q.rearIndex+1)%cap(q.data) == q.frontIndex {
return errors.New("circular queue full")
}
q.data[q.rearIndex] = value
q.rearIndex = (q.rearIndex + 1) % cap(q.data)
return nil
}
// Pop return queue a front element
func (q *CycleQueue) Pop() interface{} {
if q.rearIndex == q.frontIndex {
return nil
}
v := q.data[q.frontIndex]
q.data[q.frontIndex] = nil // 拿除元素 位置就設定為空
q.frontIndex = (q.frontIndex + 1) % cap(q.data)
return v
}
迴圈佇列一般使用空餘單元法來解決隊空和隊滿時候都存在font=rear
帶來的二義性問題,但這樣會浪費一個單元。golang
的channel
中是通過增加qcount
欄位記錄佇列長度來解決二義性,一方面不會浪費一個儲存單元,另一方面當使用len
函式檢視佇列長度時候,可以直接返回qcount
欄位,一舉兩得。
當我們需要讀取的資料的時候直接從recvx
指標上的元素取,而寫就從sendx
位置寫入元素,如圖:
sendq & recvq
當寫入資料的如果緩衝區已經滿或者讀取的緩衝區已經沒有資料的時候,就會發生協程阻塞。
如果寫阻塞的時候會把當前的協程加入到sendq
的佇列中,直到有一個recvq
發起了一個讀取的操作,那麼寫的佇列就會被程式喚醒進行工作。
當緩衝區滿了所有的g-w
則被加入sendq
佇列等待g-r
有操作就被喚醒g-w
,繼續工作,這種設計和作業系統的裡面thread
的5
種狀態很接近了,可以看出go
的設計者在可能參考過作業系統的thread
設計。
當然上面只是我簡述整個個過程,實際上go
還做了其他細節優化,sendq
不為空的時候,並且沒有緩衝區,也就是無緩衝區通道,此時會從sendq
第一個協程中拿取資料,有興趣的gopher
可以去自己檢視原始碼,本文也是最近筆者在看到這塊原始碼的筆記總結。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- Go channel 實現原理分析Go
- go 技巧: 實現一個無限 buffer 的 channelGo
- Go channel 的妙用Go
- java nio中的select和channel是怎麼使用的?Java
- Go實戰-基於Go協程和channel的使用Go
- netty系列之:channel,ServerChannel和netty中的實現NettyServer
- Go Error 巢狀到底是怎麼實現的?GoError巢狀
- go 每隔一秒 從channel 裡面拉取一下資料 要怎麼實現Go
- Go 中的 channel 與 Java BlockingQueue 的本質區別GoJavaBloC
- GO 中 string 的實現原理Go
- GO 中 slice 的實現原理Go
- GO 中 map 的實現原理Go
- GO 中 defer的實現原理Go
- Fish Redux中的Dispatch是怎麼實現的?Redux
- Go的Channel傳送和接收Go
- Go – Channel 原理Go
- go channel ->同步Go
- 圖解Go的channel底層原理圖解Go
- python中的字典賦值操作怎麼實現?Python賦值
- 在 Golang 中使用 Go 關鍵字和 Channel 實現並行Golang並行
- go : channel , queue , 程式管理 , 關閉channel ?Go
- go併發 - channelGo
- Go channel 介紹Go
- 如何優雅的關閉Go Channel「譯」Go
- go的協程及channel與web開發的一點小實踐GoWeb
- golang中channel的用法Golang
- go RWMutex 的實現GoMutex
- Golang channel底層是如何實現的?(深度好文)Golang
- 如何用 Golang 的 channel 實現訊息的批量處理Golang
- 如何用 Golang 的 channel 實現訊息的批次處理Golang
- 用 Go 語言 buffered channel 實作 Job QueueGo
- 直播美顏SDK中的美白功能是怎麼實現的?
- GO實現Redis:GO實現Redis的AOF持久化(4)GoRedis持久化
- JVM的ServerSocket是怎麼實現的(上)JVMServer
- JVM的ServerSocket是怎麼實現的(下)JVMServer
- 徹底搞懂 Channel 實現原理
- Go 閉包的實現Go
- Go 實現的 python collectionsGoPython