Go Channel的實現

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

channel作為goroutine間通訊和同步的重要途徑,是Go runtime層實現CSP並行存取模型重要的成員。在不理解底層實現時,經常在使用中對channe相關文法的表現感到疑惑,尤其是select case的行為。因此在瞭解channel的應用前先看一眼channel的實現。

Channel記憶體布局

channel是go的內建類型,它可以被儲存到變數中,可以作為函數的參數或傳回值,它在runtime層對應的資料結構式hchan。hchan維護了兩個鏈表,recvq是因讀這個chan而阻塞的G,sendq則是因寫這個chan而阻塞的G。waitq隊列中每個元素的資料結構為sudog,其中elem用於儲存資料。

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     mutex}type sudog struct {    g           *g    selectdone  *uint32    next        *sudog    prev        *sudog    elem        unsafe.Pointer // data element    releasetime int64    nrelease    int32  // -1 for acquire    waitlink    *sudog // g.waiting list}

hchan只是channel的頭部,頭部後面的一段記憶體連續的數組將作為channel的緩衝區,即用於存放channel資料的環形隊列。qcount datasize分別描述了緩衝區當前使用量和容量。若channel是無緩衝的,則size是0,就沒有這個環形隊列了。

建立chan需要知道資料類型和緩衝區大小。對應上面的結構圖newarray將產生這個環形隊列。之所以要分開指標類型緩衝區主要是為了區分gc操作,需要將它設定為flagNoScan。並且指標大小固定,可以跟hchan頭部一起分配記憶體,不需要先new(hchan)newarry

聲明但不make初始化的chan是nil chan。讀寫nil chan會阻塞,關閉nil chan會panic。

func makechan(t *chantype, size int64) *hchan {    elem := t.elem    var c *hchan    if elem.kind&kindNoPointers != 0 || size == 0 {    c = (*hchan)(mallocgc(hchanSize+uintptr(size)*uintptr(elem.size),     nil, flagNoScan))        if size > 0 && elem.size != 0 {            c.buf = add(unsafe.Pointer(c), hchanSize)        } else {            c.buf = unsafe.Pointer(c)        }    } else {        c = new(hchan)        c.buf = newarray(elem, uintptr(size))    }    c.elemsize = uint16(elem.size)    c.elemtype = elem    c.dataqsiz = uint(size)    return c}

Channel操作

從實現中可見讀寫chan都要lock,這跟讀寫共用記憶體一樣都有lock的開銷。

資料在chan中的傳遞方向從chansend開始從入參最終寫入recvq中的goroutine的資料域,這中間如果發生阻塞可能先寫入sendq中goroutine的資料域等待中轉。

從gopark返回後sudog對象可重用。

同步讀寫

寫channel c<-x 調用runtime.chansend。讀channel <-c 調用runtime.chanrecv。總結同步讀寫的過程就是:

  • 寫chan時優先檢查recvq中有沒有等待讀chan的goroutine,若有從recvq中出隊sudoG。syncsend將要寫入chan的資料ep複製給剛出隊的sudoG的elem域。通過goready喚醒接收者G,狀態設定為_Grunnable,之後放進P本地待運行隊列。之後這個讀取到資料的G可以再次被P調度了。
  • 寫chan時如果沒有G等待讀,當前G因等待寫而阻塞。這時建立或擷取acquireSudog,封裝上要寫入的資料進入sendq隊列。同時當前Ggopark休眠等待被喚醒。
  • 讀chan時優先喚醒sendq中等待寫的goroutine,並從中擷取資料;若沒人寫則將自己掛到recvq中等待喚醒。
func chansend(t *chantype, c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {...    lock(&c.lock)    if c.dataqsiz == 0 { // synchronous channel        sg := c.recvq.dequeue()        if sg != nil { // found a waiting receiver            unlock(&c.lock)            recvg := sg.g            syncsend(c, sg, ep)            goready(recvg, 3)            return true        }        // no receiver available: block on this channel.        mysg := acquireSudog()        mysg.elem = ep          c.sendq.enqueue(mysg)        goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)        // someone woke us up.        releaseSudog(mysg)        return true    }}
func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {    if c.dataqsiz == 0 { // synchronous channel        sg := c.sendq.dequeue()        if sg != nil {            unlock(&c.lock)            typedmemmove(c.elemtype, ep, sg.elem)            gp.param = unsafe.Pointer(sg)            goready(gp, 3)            return true, true        }        // no sender available: block on this channel.        mysg := acquireSudog()        mysg.elem = ep        c.recvq.enqueue(mysg)        goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)        // someone woke us up        releaseSudog(mysg)        return recvclosed(c, ep)    }}

非同步讀寫

非同步與同步的區別就是讀寫時會優先檢查緩衝區有沒有資料讀或有沒有空間寫。並且真正讀寫chan後會發生緩衝區變化,這時可能之前阻塞的goroutine有機會寫和讀了,所以要嘗試喚醒它們。 總結過程:

  • 寫chan時緩衝區已滿,則將當前G和資料封裝好放入sendq隊列中等待寫入,同時掛起gopark當前goroutine。若緩衝區未滿,則直接將資料寫入緩衝區,並更新緩衝區最新資料的index以及qcount。同時嘗試從recvq中喚醒goready一個之前因為緩衝區無資料可讀而阻塞的等待讀的goroutine。
  • 讀chan時首先看緩衝區有沒有資料,若有則直接讀取,並嘗試喚醒一個之前因為緩衝區滿而阻塞的等待寫的goroutine,讓它有機會寫資料。若無資料可讀則入隊recvq。
func chansend(t *chantype, c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {    // asynchronous channel    var t1 int64    for futile := byte(0); c.qcount >= c.dataqsiz; futile = traceFutileWakeup {        mysg := acquireSudog()        c.sendq.enqueue(mysg)        goparkunlock(&c.lock, "chan send", traceEvGoBlockSend|futile, 3)        // someone woke us up - try again        releaseSudog(mysg)    }    // write our data into the channel buffer    typedmemmove(c.elemtype, chanbuf(c, c.sendx), ep)    c.sendx++    if c.sendx == c.dataqsiz {        c.sendx = 0    }    c.qcount++    // wake up a waiting receiver    sg := c.recvq.dequeue()    if sg != nil {        goready(sg.g, 3)    }     return true}
func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {    // asynchronous channel    for futile := byte(0); c.qcount <= 0; futile = traceFutileWakeup {        mysg := acquireSudog()        c.recvq.enqueue(mysg)        goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv|futile, 3)        // someone woke us up - try again        releaseSudog(mysg)    }    typedmemmove(c.elemtype, ep, chanbuf(c, c.recvx))    memclr(chanbuf(c, c.recvx), uintptr(c.elemsize))    c.recvx++    if c.recvx == c.dataqsiz {        c.recvx = 0    }    c.qcount--    // ping a sender now that there is space    sg := c.sendq.dequeue()    if sg != nil {        goready(sg.g, 3)    }    return true, true}

關閉

通過goready喚醒recvq中等待讀的goroutine,之後喚醒所有sendq中等待寫的goroutine。因此close chan相當於解除所有因它阻塞的gouroutine的阻塞。

func closechan(c *hchan) {    c.closed = 1    // release all readers    for {        sg := c.recvq.dequeue()        if sg == nil {            break        }...        goready(gp, 3)    }    // release all writers    for {        sg := c.sendq.dequeue()        if sg == nil {            break        }...        goready(gp, 3)    }}

寫closed chan或關閉 closed chan會導致panic。讀closed chan永遠不會阻塞,會返回一個通道資料類型的零值,返回給函數的參數ep。

所以通常在close chan時需要通過讀操作來判斷chan是否關閉。

if v, open := <- c; !open {   // chan is closed}

Happens before

在go memory model 裡講了happens-before問題很有意思。其中有一些跟chan相關的同步規則可以解釋一些一直以來的疑問,記錄如下:

  • 對帶緩衝chan的寫操作 happens-before相應chan的讀操作
  • 關閉chan happens-before 從該chan讀最後的傳回值0
  • 不帶緩衝的chan的讀操作 happens-before相應chan的寫操作
var c = make(chan int, 10)var a stringfunc f() {  a = "hello, world"  //(1)  c <- 0  // (2)}func main() {  go f()  <- c  //(3)  print(a)  //(4)}

(1) happens-before(2) (3) happens-before(4),再根據規則可知(2) happens(3)。因此(1)happens-before(4),這段代碼沒有問題,肯定會輸出hello world。

var c = make(chan int)var a stringfunc f() {  a = "hello, world"  //(1)  <-c  // (2)}func main() {  go f()  c <- 0  //(3)  print(a)  //(4)}

同樣根據規則三可知(2)happens-before(3) 最終可以保證(1) happens-before(4)。若c改成待緩衝的chan,則結果將不再有任何同步保證使得(2) happens-before(3)。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.