一文讀懂channel設(shè)計(jì)

在Go中,要理解channel,首先需要認(rèn)識goroutine。

一、為什么會有g(shù)oroutine

現(xiàn)代操作系統(tǒng)中為我們提供了三種基本的構(gòu)造并發(fā)程序的方法:多進(jìn)程、I/O多路復(fù)用和多線程。其中最簡單的構(gòu)造方式當(dāng)屬多進(jìn)程,但是多進(jìn)程的并發(fā)程序,由于對進(jìn)程控制和進(jìn)程間通信開銷巨大,這樣的并發(fā)方式往往會很慢。

因此,操作系統(tǒng)提供了更小粒度的運(yùn)行單元:線程(確切叫法是內(nèi)核線程)。它是一種運(yùn)行在進(jìn)程上下文中的邏輯流,線程之間通過操作系統(tǒng)來調(diào)度,其調(diào)度模型如下圖所示。

1.png

多線程的并發(fā)方式,相較于多進(jìn)程而言要快得多。但是由于線程上下文切換總是不可避免的陷入內(nèi)核態(tài),它的開銷依然較大。那么有沒有不必陷入內(nèi)核態(tài)的運(yùn)行載體呢?有,用戶級線程。 用戶級線程的切換由用戶程序自己控制,不需要內(nèi)核干涉,因此少了進(jìn)出內(nèi)核態(tài)的消耗。

2.png

這里的用戶級線程就是協(xié)程(coroutine),它們的切換由運(yùn)行時系統(tǒng)來統(tǒng)一調(diào)度管理,內(nèi)核并不知道它的存在。協(xié)程是抽象于內(nèi)核線程之上的對象,一個內(nèi)核線程可以對應(yīng)多個協(xié)程。但最終的系統(tǒng)調(diào)用仍然需要內(nèi)核線程來完成。注意,線程的調(diào)度是操作系統(tǒng)來管理,是一種搶占式調(diào)度。而協(xié)程不同,協(xié)程之間需要合作,會主動交出執(zhí)行權(quán),是一種協(xié)作式調(diào)度,這也是為何被稱為協(xié)程的原因。

Go天生在語言層面支持了協(xié)程,即我們常說的goroutine。Go的runtime系統(tǒng)實(shí)現(xiàn)的是一種M:N調(diào)度模型,通過GMP對象來描述,其中G代表的就是協(xié)程,M是線程,P是調(diào)度上下文。在Go程序中,一個goroutine就代表著一個最小用戶代碼執(zhí)行流,它們也是并發(fā)流的最小單元。

二、channel的存在定位

從內(nèi)存的角度而言,并發(fā)模型只分兩種:基于共享內(nèi)存和基于消息通信(內(nèi)存拷貝)。在Go中,兩種并發(fā)模型的同步原語均有提供:sync.*和atomic.*代表的就是基于共享內(nèi)存;channel代表的就是基于消息通信。而Go提倡后者,它包括三大元素:goroutine(執(zhí)行體),channel(通信),select(協(xié)調(diào))。

Do not communicate by sharing memory; instead, share memory by communicating.

在Go中通過goroutine+channel的方式,可以簡單、高效地解決并發(fā)問題,channel就是goroutine之間的數(shù)據(jù)橋梁。

Concurrency is the key to designing high performance network services. Go's concurrency primitives (goroutines and channels) provide a simple and efficient means of expressing concurrent execution.

以下是一個簡單的channel使用示例代碼。

func goroutineA(ch <-chan int)  {
    fmt.Println("[goroutineA] want a data")
    val := <- ch
    fmt.Println("[goroutineA] received the data", val)
}

func goroutineB(ch chan<- int)  {
    time.Sleep(time.Second*1)
    ch <- 1
    fmt.Println("[goroutineB] send the data 1")
}

func main() {
    ch := make(chan int, 1)
    go goroutineA(ch)
    go goroutineB(ch)
    time.Sleep(2*time.Second)
}

上述過程趣解圖如下

3.png
4.png
5.png
6.png

三、channel源碼解析

channel源碼位于src/go/runtime/chan.go。本章內(nèi)容分為兩部分:channel內(nèi)部結(jié)構(gòu)和channel操作。

3.1 channel內(nèi)部結(jié)構(gòu)

ch := make(chan int,2)

對于以上channel的申明語句,我們可以在程序中加入斷點(diǎn),得到ch的信息如下。

7.png

很好,看起來非常的清晰。但是,這些信息代表的是什么含義呢?接下來,我們先看幾個重要的結(jié)構(gòu)體。

  • hchan

當(dāng)我們通過make(chan Type, size)生成channel時,在runtime系統(tǒng)中,生成的是一個hchan結(jié)構(gòu)體對象。源碼位于src/runtime/chan.go

type hchan struct {
    qcount   uint           // 循環(huán)隊(duì)列中數(shù)據(jù)數(shù)
    dataqsiz uint           // 循環(huán)隊(duì)列的大小
    buf      unsafe.Pointer // 指向大小為dataqsize的包含數(shù)據(jù)元素的數(shù)組指針
    elemsize uint16         // 數(shù)據(jù)元素的大小
    closed   uint32         // 代表channel是否關(guān)閉   
    elemtype *_type         // _type代表Go的類型系統(tǒng),elemtype代表channel中的元素類型
    sendx    uint           // 發(fā)送索引號,初始值為0
    recvx    uint           // 接收索引號,初始值為0
  recvq    waitq          // 接收等待隊(duì)列,存儲試圖從channel接收數(shù)據(jù)(<-ch)的阻塞goroutines
    sendq    waitq          // 發(fā)送等待隊(duì)列,存儲試圖發(fā)送數(shù)據(jù)(ch<-)到channel的阻塞goroutines

    lock mutex              // 加鎖能保護(hù)hchan的所有字段,包括waitq中sudoq對象
}
  • waitq

waitq用于表達(dá)處于阻塞狀態(tài)的goroutines鏈表信息,first指向鏈頭goroutine,last指向鏈尾goroutine

type waitq struct {
    first *sudog           
    last  *sudog
}
  • sudug

sudog代表的就是一個處于等待列表中的goroutine對象,源碼位于src/runtime/runtime2.go

type sudog struct {
    g *g
    next *sudog
    prev *sudog
    elem unsafe.Pointer // data element (may point to stack)
    c        *hchan // channel
  ...
}

為了更好理解hchan結(jié)構(gòu)體,我們將通過以下代碼來理解hchan中的字段含義。

package main

import "time"

func goroutineA(ch chan int) {
    ch <- 100
}

func goroutineB(ch chan int) {
    ch <- 200
}

func goroutineC(ch chan int) {
    ch <- 300
}

func goroutineD(ch chan int) {
    ch <- 300
}

func main() {
    ch := make(chan int, 4)
    for i := 0; i < 4; i++ {
        ch <- i * 10
    }
    go goroutineA(ch)
    go goroutineB(ch)
    go goroutineC(ch)
    go goroutineD(ch)
    // 第一個sleep是為了給上足夠的時間讓所有g(shù)oroutine都已啟動
    time.Sleep(time.Millisecond * 500)
    time.Sleep(time.Second)
}

打開代碼調(diào)試功能,將程序運(yùn)行至斷點(diǎn)time.Sleep(time.Second)處,此時得到的chan信息如下。

8.png

在該channel中,通過make(chan int, 4)定義的channel大小為4,即dataqsiz的值為4。同時由于循環(huán)隊(duì)列中已經(jīng)添加了4個元素,所以qcount值也為4。此時,有4個goroutine(A-D)想發(fā)送數(shù)據(jù)給channel,但是由于存放數(shù)據(jù)的循環(huán)隊(duì)列已滿,所以只能進(jìn)入發(fā)送等待列表,即sendq。同時要注意到,此時的發(fā)送和接收索引值均為0,即下一次接收數(shù)據(jù)的goroutine會從循環(huán)隊(duì)列的第一個元素拿,發(fā)送數(shù)據(jù)的goroutine會發(fā)送到循環(huán)隊(duì)列的第一個位置。

上述hchan結(jié)構(gòu)可視化圖解如下

9.png

3.2 channel操作

將channel操作分為四部分:創(chuàng)建、發(fā)送、接收和關(guān)閉。

創(chuàng)建

本文的參考Go版本為1.15.2。其channel的創(chuàng)建實(shí)現(xiàn)代碼位于src/go/runtime/chan.go的makechan方法。

func makechan(t *chantype, size int) *hchan {
    elem := t.elem

  // 發(fā)送元素大小限制
    if elem.size >= 1<<16 {
        throw("makechan: invalid channel element type")
    }
  // 對齊檢查
    if hchanSize%maxAlign != 0 || elem.align > maxAlign {
        throw("makechan: bad alignment")
    }

  // 判斷是否會內(nèi)存溢出
    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

  // 為構(gòu)造的hchan對象分配內(nèi)存
    var c *hchan
    switch {
  // 無緩沖的channel或者元素大小為0的情況
    case mem == 0:
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        c.buf = c.raceaddr()
  // 元素不包含指針的情況  
    case elem.ptrdata == 0:
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)
  // 元素包含指針  
    default:
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

  // 初始化相關(guān)參數(shù)
    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
}

可以看到,makechan方法主要就是檢查傳送元素的合法性,并為hchan分配內(nèi)存,初始化相關(guān)參數(shù),包括對鎖的初始化。

發(fā)送

channel的發(fā)送實(shí)現(xiàn)代碼位于src/go/runtime/chan.go的chansend方法。發(fā)送過程,存在以下幾種情況。

  1. 當(dāng)發(fā)送的channel為nil
if c == nil {
    if !block {
        return false
    }
    gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
    throw("unreachable")
}

往一個nil的channel中發(fā)送數(shù)據(jù)時,調(diào)用gopark函數(shù)將當(dāng)前執(zhí)行的goroutine從running態(tài)轉(zhuǎn)入waiting態(tài)。

  1. 往已關(guān)閉的channel中發(fā)送數(shù)據(jù)
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

如果向已關(guān)閉的channel中發(fā)送數(shù)據(jù),會引發(fā)panic。

  1. 如果已經(jīng)有阻塞的接收goroutines(即recvq中指向非空),那么數(shù)據(jù)將被直接發(fā)送給接收goroutine。
if sg := c.recvq.dequeue(); sg != nil {
    // Found a waiting receiver. We pass the value we want to send
    // directly to the receiver, bypassing the channel buffer (if any).
    send(c, sg, ep, func() { unlock(&c.lock) }, 3)
    return true
}

該邏輯的實(shí)現(xiàn)代碼在send方法和sendDirect中。

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
  ... // 省略了競態(tài)代碼
    if sg.elem != nil {
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    goready(gp, skip+1)
}

func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
    dst := sg.elem
    typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
    memmove(dst, src, t.size)
}

其中,memmove我們已經(jīng)在源碼系列中遇到多次了,它的目的是將內(nèi)存中src的內(nèi)容拷貝至dst中去。另外,注意到goready(gp, skip+1)這句代碼,它會使得之前在接收等待隊(duì)列中的第一個goroutine的狀態(tài)變?yōu)閞unnable,這樣go的調(diào)度器就可以重新讓該goroutine得到執(zhí)行。

  1. 對于有緩沖的channel來說,如果當(dāng)前緩沖區(qū)hchan.buf有可用空間,那么會將數(shù)據(jù)拷貝至緩沖區(qū)
if c.qcount < c.dataqsiz {
    qp := chanbuf(c, c.sendx)
    if raceenabled {
        raceacquire(qp)
        racerelease(qp)
    }
    typedmemmove(c.elemtype, qp, ep)
  // 發(fā)送索引號+1
    c.sendx++
  // 因?yàn)榇鎯?shù)據(jù)元素的結(jié)構(gòu)是循環(huán)隊(duì)列,所以當(dāng)當(dāng)前索引號已經(jīng)到隊(duì)末時,將索引號調(diào)整到隊(duì)頭
    if c.sendx == c.dataqsiz {
        c.sendx = 0
    }
  // 當(dāng)前循環(huán)隊(duì)列中存儲元素?cái)?shù)+1
    c.qcount++
    unlock(&c.lock)
    return true
}

其中,chanbuf(c, c.sendx)是獲取指向?qū)?yīng)內(nèi)存區(qū)域的指針。typememmove會調(diào)用memmove方法,完成數(shù)據(jù)的拷貝工作。另外注意到,當(dāng)對hchan進(jìn)行實(shí)際操作時,是需要調(diào)用lock(&c.lock)加鎖,因此,在完成數(shù)據(jù)拷貝后,通過unlock(&c.lock)將鎖釋放。

  1. 有緩沖的channel,當(dāng)hchan.buf已滿;或者無緩沖的channel,當(dāng)前沒有接收的goroutine
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
    mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)

通過getg獲取當(dāng)前執(zhí)行的goroutine。acquireSudog是先獲得當(dāng)前執(zhí)行g(shù)oroutine的線程M,再獲取M對應(yīng)的P,最后將P的sudugo緩存隊(duì)列中的隊(duì)頭sudog取出(詳見源碼src/runtime/proc.go)。通過c.sendq.enqueue將sudug加入到channel的發(fā)送等待列表中,并調(diào)用gopark將當(dāng)前goroutine轉(zhuǎn)為waiting態(tài)。

  • 發(fā)送操作會對hchan加鎖。
  • 當(dāng)recvq中存在等待接收的goroutine時,數(shù)據(jù)元素將會被直接拷貝給接收goroutine。
  • 當(dāng)recvq等待隊(duì)列為空時,會判斷hchan.buf是否可用。如果可用,則會將發(fā)送的數(shù)據(jù)拷貝至hchan.buf中。
  • 如果hchan.buf已滿,那么將當(dāng)前發(fā)送goroutine置于sendq中排隊(duì),并在運(yùn)行時中掛起。
  • 向已經(jīng)關(guān)閉的channel發(fā)送數(shù)據(jù),會引發(fā)panic。

對于無緩沖的channel來說,它天然就是hchan.buf已滿的情況,因?yàn)樗膆chan.buf的容量為0。

package main

import "time"

func main() {
    ch := make(chan int)
    go func(ch chan int) {
        ch <- 100
    }(ch)
    time.Sleep(time.Millisecond * 500)
    time.Sleep(time.Second)
}

在上述示例中,發(fā)送goroutine向無緩沖的channel發(fā)送數(shù)據(jù),但是沒有接收goroutine。將斷點(diǎn)置于time.Sleep(time.Second),得到此時ch結(jié)構(gòu)如下。

10.png

可以看到,在無緩沖的channel中,其hchan的buf長度為0,當(dāng)沒有接收groutine時,發(fā)送的goroutine將被置于sendq的發(fā)送隊(duì)列中。

接收

channel的接收實(shí)現(xiàn)分兩種,v :=<-ch對應(yīng)于chanrecv1,v, ok := <- ch對應(yīng)于chanrecv2,但它們都依賴于位于src/go/runtime/chan.go的chanrecv方法。

func chanrecv1(c *hchan, elem unsafe.Pointer) {
    chanrecv(c, elem, true)
}

func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
    _, received = chanrecv(c, elem, true)
    return
}

chanrecv的詳細(xì)代碼此處就不再展示,和chansend邏輯對應(yīng),具體處理準(zhǔn)則如下。

  • 接收操作會對hchan加鎖。
  • 當(dāng)sendq中存在等待發(fā)送的goroutine時,意味著此時的hchan.buf已滿(無緩存的天然已滿),分兩種情況(見代碼src/go/runtime/chan.go的recv方法):1. 如果是有緩存的hchan,那么先將緩沖區(qū)的數(shù)據(jù)拷貝給接收goroutine,再將sendq的隊(duì)頭sudog出隊(duì),將出隊(duì)的sudog上的元素拷貝至hchan的緩存區(qū)。 2. 如果是無緩存的hchan,那么直接將出隊(duì)的sudog上的元素拷貝給接收goroutine。兩種情況的最后都會喚醒出隊(duì)的sudog上的發(fā)送goroutine。
  • 當(dāng)sendq發(fā)送隊(duì)列為空時,會判斷hchan.buf是否可用。如果可用,則會將hchan.buf的數(shù)據(jù)拷貝給接收goroutine。
  • 如果hchan.buf不可用,那么將當(dāng)前接收goroutine置于recvq中排隊(duì),并在運(yùn)行時中掛起。
  • 與發(fā)送不同的是,當(dāng)channel關(guān)閉時,goroutine還能從channel中獲取數(shù)據(jù)。如果recvq等待列表中有g(shù)oroutines,那么它們都會被喚醒接收數(shù)據(jù)。如果hchan.buf中還有未接收的數(shù)據(jù),那么goroutine會接收緩沖區(qū)中的數(shù)據(jù),否則goroutine會獲取到元素的零值。

以下是channel關(guān)閉之后,接收goroutine的讀取示例代碼。

func main() {
    ch := make(chan int, 1)
    ch <- 10
    close(ch)
    a, ok := <-ch
    fmt.Println(a, ok)
    b, ok := <-ch
    fmt.Println(b, ok)
    c := <-ch
    fmt.Println(c)
}

//輸出如下
10 true
0 false
0

注意:在channel中進(jìn)行的所有元素轉(zhuǎn)移都伴隨著內(nèi)存的拷貝。

func main() {
    type Instance struct {
        ID   int
        name string
    }

    var ins = Instance{ID: 1, name: "Golang"}

    ch := make(chan Instance, 3)
    ch <- ins

    fmt.Println("ins的原始值:", ins)

    ins.name = "Python"
    go func(ch chan Instance) {
        fmt.Println("channel接收值:", <-ch)
    }(ch)

    time.Sleep(time.Second)
    fmt.Println("ins的最終值:", ins)
}

// 輸出結(jié)果
ins的原始值: {1 Golang}
channel接收值: {1 Golang}
ins的最終值: {1 Python}

前半段圖解如下

11.png

后半段圖解如下

12.png

注意,如果把channel傳遞類型替換為Instance指針時,那么盡管channel存入到buf中的元素已經(jīng)是拷貝對象了,從channel中取出又被拷貝了一次。但是由于它們的類型是Instance指針,拷貝對象與原始對象均會指向同一個內(nèi)存地址,修改原有元素對象的數(shù)據(jù)時,會影響到取出數(shù)據(jù)。

func main() {
    type Instance struct {
        ID   int
        name string
    }

    var ins = &Instance{ID: 1, name: "Golang"}

    ch := make(chan *Instance, 3)
    ch <- ins

    fmt.Println("ins的原始值:", ins)

    ins.name = "Python"
    go func(ch chan *Instance) {
        fmt.Println("channel接收值:", <-ch)
    }(ch)

    time.Sleep(time.Second)
    fmt.Println("ins的最終值:", ins)
}

// 輸出結(jié)果
ins的原始值: &{1 Golang}
channel接收值: &{1 Python}
ins的最終值: &{1 Python}

因此,在使用channel時,盡量避免傳遞指針,如果傳遞指針,則需謹(jǐn)慎。

關(guān)閉

channel的關(guān)閉實(shí)現(xiàn)代碼位于src/go/runtime/chan.go的chansend方法,詳細(xì)執(zhí)行邏輯已通過注釋寫明。

func closechan(c *hchan) {
  // 如果hchan對象為nil,則會引發(fā)painc
    if c == nil {
        panic(plainError("close of nil channel"))
    }

  // 對hchan加鎖
    lock(&c.lock)
  // 不同多次調(diào)用close(c chan<- Type)方法,否則會引發(fā)painc
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

    if raceenabled {
        callerpc := getcallerpc()
        racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
        racerelease(c.raceaddr())
    }

  // close標(biāo)志
    c.closed = 1

  // gList代表Go的GMP調(diào)度的G集合
    var glist gList

    // 該for循環(huán)是為了釋放recvq上的所有等待接收sudog
    for {
        sg := c.recvq.dequeue()
        if sg == nil {
            break
        }
        if sg.elem != nil {
            typedmemclr(c.elemtype, sg.elem)
            sg.elem = nil
        }
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = nil
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }

    // 該for循環(huán)會釋放sendq上的所有等待發(fā)送sudog
    for {
        sg := c.sendq.dequeue()
        if sg == nil {
            break
        }
        sg.elem = nil
        if sg.releasetime != 0 {
            sg.releasetime = cputicks()
        }
        gp := sg.g
        gp.param = nil
        if raceenabled {
            raceacquireg(gp, c.raceaddr())
        }
        glist.push(gp)
    }
  // 釋放sendq和recvq之后,hchan釋放鎖
    unlock(&c.lock)

  // 將上文中g(shù)list中的加入的goroutine取出,讓它們均變?yōu)閞unnable(可執(zhí)行)狀態(tài),等待調(diào)度器執(zhí)行
    // 注意:我們上文中分析過,試圖向一個已關(guān)閉的channel發(fā)送數(shù)據(jù),會引發(fā)painc。
  // 所以,如果是釋放sendq中的goroutine,它們一旦得到執(zhí)行將會引發(fā)panic。
    for !glist.empty() {
        gp := glist.pop()
        gp.schedlink = 0
        goready(gp, 3)
    }
}

關(guān)于關(guān)閉操作,有幾個點(diǎn)需要注意一下。

  • 如果關(guān)閉已關(guān)閉的channel會引發(fā)painc。
  • 對channel關(guān)閉后,如果有阻塞的讀取或發(fā)送goroutines將會被喚醒。讀取goroutines會獲取到hchan的已接收元素,如果沒有,則獲取到元素零值;發(fā)送goroutine的執(zhí)行則會引發(fā)painc。

對于第二點(diǎn),我們可以很好利用這一特性來實(shí)現(xiàn)對程序執(zhí)行流的控制(類似于sync.WaitGroup的作用),以下是示例程序代碼。

func main() {
    ch := make(chan struct{})
    //
    go func() {
        // do something work...
        // when work has done, call close()
        close(ch)
    }()
    // waiting work done
    <- ch
    // other work continue...
}

四、總結(jié)

channel是Go中非常強(qiáng)大有用的機(jī)制,為了更有效地使用它,我們必須了解它的實(shí)現(xiàn)原理,這也是寫作本文的目的。

  • hchan結(jié)構(gòu)體有鎖的保證,對于并發(fā)goroutine而言是安全的
  • channel接收、發(fā)送數(shù)據(jù)遵循FIFO(First In First Out)原語
  • channel的數(shù)據(jù)傳遞依賴于內(nèi)存拷貝
  • channel能阻塞(gopark)、喚醒(goready)goroutine
  • 所謂無緩存的channel,它的工作方式就是直接發(fā)送goroutine拷貝數(shù)據(jù)給接收goroutine,而不通過hchan.buf

另外,可以看到Go在channel的設(shè)計(jì)上權(quán)衡了簡單與性能。為了簡單性,hchan是有鎖的結(jié)構(gòu),因?yàn)橛墟i的隊(duì)列會更易理解和實(shí)現(xiàn),但是這樣會損失一些性能??紤]到整個 channel 操作帶鎖的成本較高,其實(shí)官方也曾考慮過使用無鎖 channel 的設(shè)計(jì),但是由于目前已有提案中(https://github.com/golang/go/issues/8899),無鎖實(shí)現(xiàn)的channel可維護(hù)性差、且實(shí)際性能測試不具有說服力,而且也不符合Go的簡單哲學(xué),因此官方目前為止并沒有采納無鎖設(shè)計(jì)。

在性能上,有一點(diǎn),我們需要認(rèn)識到:所謂channel中阻塞goroutine,只是在runtime系統(tǒng)中被blocked,它是用戶層的阻塞。而實(shí)際的底層內(nèi)核線程不受影響,它仍然是unblocked的。

參考鏈接

https://speakerdeck.com/kavya719/understanding-channels

https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8

https://github.com/talkgo/night/issues/450

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容