Golang Channel源碼解析

channels

channel存在以下四個特性

  • goroutine-safe
  • store and pass values between goroutines
  • provide FIFO semantics
  • can cause goroutines to block and unblock

How to use Channels

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 3)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
			time.Sleep(500 * time.Millisecond)
		}
		close(ch)
	}()

	value := <-ch
	fmt.Println(value)
	for value := range ch {
		fmt.Println(value)
	}
}

Channel源碼解析

基於的go版本爲1.13,源代碼位於runtime/chan.go

代碼入口

go的編譯器對channel進行處理,從彙編中可以看到具體調用的函數。下面截取了部分的關鍵代碼。

$ go tool compile -S channel.go > channel.s
$ vim channel.s
0x0035 00053 (channel.go:9) CALL    runtime.makechan(SB)
0x003e 00062 (channel.go:12)    CALL    runtime.chansend1(SB)
0x0078 00120 (channel.go:18)    CALL    runtime.chanrecv1(SB)
0x0067 00103 (channel.go:15)    CALL    runtime.closechan(SB)

從上面可以看出

  • 當調用make(chan int,4)時,調用的是runtime下的makechan函數
  • 當調用ch <- i時,調用的是runtime下的chansend1函數
  • 當調用value := <-ch時,調用的是runtime下的chanrecv函數

channel的結構體

type hchan struct {
	qcount   uint           // channel的隊列中數據的總數,會隨着<-和 -> 變化
	dataqsiz uint           // channel循環數組的長度,
	buf      unsafe.Pointer // 指向底層循環數組的指針,只針對緩衝channel
	elemsize uint16   //元素大小
	closed   uint32    //channel是否被關閉的標誌
	elemtype *_type // 元素類型
	sendx    uint   // 發送元素在循環數組中的索引
	recvx    uint   // 接收元素在循環數組中的索引
	recvq    waitq  // 等待接收的goroutine隊列
	sendq    waitq  // 等待發送的goroutine隊列

	//保護hchan中的所有字段以及recvq,sendq中的sudogs
	lock mutex
}

//waitq是隊列,用於存儲sudog
type waitq struct {
	first *sudog
	last  *sudog
}

新建channel

從上面的彙編可以看到,當執行make指令時,go會調用makechan函數來創建一個hchan的結構體

輸入的參數是channel的類型和長度,返回一個指向hchan的指針
const (
    hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
)

//malloc.go
func func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer 

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

    // 忽略一些前置檢查,分析關鍵部分的代碼
    ...
    //MulUintptr返回elem.size*uintptr(size)的值,並判斷是否溢出
    mem, overflow := math.MulUintptr(elem.size, uintptr(size))
    if overflow || mem > maxAlloc-hchanSize || size < 0 {
        panic(plainError("makechan: size out of range"))
    }

    var c *hchan
    switch {
    case mem == 0:
        // mem爲0,說明該channel是無緩衝,申請的長度爲hchanSize(hchan的基本長度)
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        c.buf = c.raceaddr()
    case elem.ptrdata == 0:
        // elem中並不存在指針,申請的總長度即爲hchanSize+mem
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        //buf指向c+hchanSize的位置
        c.buf = add(unsafe.Pointer(c), hchanSize)
    default:
        // elem中包含指針
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }
    //複製elem的大小,類型,並設置循環數組的總長度
    c.elemsize = uint16(elem.size)
    c.elemtype = elem
    c.dataqsiz = uint(size)
    //
    ...
    return c
}

channel發送元素

從上面的彙編可以看到,當執行ch <- i 指令時,go會調用chansend1函數,從函數可以看出,chansend1實際調用的是chansend函數。

// entry point for c <- x from compiled code
//go:nosplit
//go:nosplit用來指定文件中聲明的函數不得進行堆棧溢出檢查,這是一個在不安全搶佔調用goroutine時常用的做法。
func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    //如果channel爲空
	if c == nil {
	    //如果是非阻塞的,直接返回false
		if !block {
			return false
		}
		//當前的goroutine被掛起
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	//對於非阻塞情況,進行fast check的操作
	//當滿足條件爲 1.非阻塞 2.channel未關閉 3.無緩衝channel&&沒有等待的goroutine 4.緩衝channel並且循環數組已經滿了
	//則直接返回false
	if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
		(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
		return false
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	lock(&c.lock)

    //如果channel已經被關閉,則拋出異常
    //考點:往關閉的channel發送數據,會導致拋出異常
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

    //如果channel的等待goroutine隊列不爲空,說明有goroutine在等待接收值
	if sg := c.recvq.dequeue(); sg != nil {
		//將ep的值進行直接拷貝,繞過channel的buffer緩存
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

    //如果當前循環數組還沒滿,則將元素入隊
	if c.qcount < c.dataqsiz {
	    //返回sendx指向的指針位置
		qp := chanbuf(c, c.sendx)
		//將元素進行拷貝
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		unlock(&c.lock)
		return true
	}

    //如果爲非阻塞,則直接返回
	if !block {
		unlock(&c.lock)
		return false
	}

	//如果爲阻塞channel,並且當前的循環數組已經滿了,則將當前的gouroutine封裝成sudog入隊。
	//獲取當前goroutine
	gp := getg()
	//獲取一個sudog
	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
	//將goroutine對應的sudog入隊,並掛起goroutine
	c.sendq.enqueue(mysg)
	goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)

	KeepAlive(ep)

	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if gp.param == nil {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	gp.param = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	mysg.c = nil
	releaseSudog(mysg)
	return true
}

channel讀取元素

從上面的彙編可以看到,當執行ch <- i 指令時,go會調用chanrecv1函數,從函數可以看出,chanrecv1實際調用的是chanrecv函數。核心邏輯和發送元素一致。

// entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

// 如果block == false 並且沒有對應的元素, 則返回(false, false).
// 如果channel被關閉了, 則對ep的元素置零,並返回 (true, false).
// 否則將指針ep指向的值進行賦值並返回(true, true)
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    //如果channel爲空
	if c == nil {
	    //該channel爲非阻塞
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

    //對於非阻塞情況,進行fast check的操作
	//當滿足條件爲 1.非阻塞 2.非緩衝channel並且沒有等待發送的goroutine
	//3.緩衝channel,但是循環數組爲空 4.channel未關閉
	//則直接返回false
	if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
		c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
		atomic.Load(&c.closed) == 0 {
		return
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	lock(&c.lock)

    //如果當前channel已經被關閉,並且循環數組爲空,
	if c.closed != 0 && c.qcount == 0 {
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}

    //如果發送隊列中存在等待的goroutine,則將sg中的值直接複製給ep
	if sg := c.sendq.dequeue(); sg != nil {
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

    // 如果當前循環數組不爲空
	if c.qcount > 0 {
		//返回recvx指向的指針位置
		qp := chanbuf(c, c.recvx)
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		typedmemclr(c.elemtype, qp)
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.qcount--
		unlock(&c.lock)
		return true, true
	}

    //如果爲非阻塞的channel,直接返回
	if !block {
		unlock(&c.lock)
		return false, false
	}

	//和send一樣,將當前的goroutine封裝成sudog,併入隊
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}

	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.param = nil
	c.recvq.enqueue(mysg)
	goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)

	// someone woke us up
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	closed := gp.param == nil
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, !closed
}

關閉channel

從上面的彙編可以看到,當執行ch <- i 指令時,go會調用closechan函數

func closechan(c *hchan) {
    //如果channel爲空,則拋出panic
	if c == nil {
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
	//如果當前channel已經被關閉了,則會拋出異常
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}
    //設置closed標誌位爲1
	c.closed = 1

	var glist gList

	// 將recv隊列中的sudog進行出隊,並將elem置空,將gouroutine放入glist列表中
	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
		glist.push(gp)
	}

	// 同理,將send隊列中的sudog出隊,將elem置空,並將goroutine放入glist中
	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
		glist.push(gp)
	}
	unlock(&c.lock)

	// 將從上面獲取的所有goroutine的狀態置爲Ready
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

select channel

channel reveive value

對於select下的channel接收元素,

select {
	case c <- v:
	    ... foo
	default:
	    ... bar
}

go的編譯器將它轉換成selectnbsend

if selectnbsend(c, v) {
    ... foo
} else {
	... bar
}

func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
	return chansend(c, elem, false, getcallerpc())
}	

channel send value

對於select下的發送value場景

select {
	case v = <-c:
		... foo
	default:
		... bar
}

go的編譯器將它轉換成selectnbrevc

if selectnbrecv(&v, c) {
	... foo
} else {
	... bar
}

func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
	selected, _ = chanrecv(c, elem, false)
	return
}

用於判斷channel是否關閉的場景

select {
	case v, ok = <-c:
		... foo
	default:
		... bar
}

golang編譯器會將它轉換成selectnbrecv2

if c != nil && selectnbrecv2(&v, &ok, c) {
	... foo
} else {
	... bar
}

func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
	// TODO(khr): just return 2 values from this function, now that it is in Go.
	selected, *received = chanrecv(c, elem, false)
	return
}

https://speakerd.s3.amazonaws.com/presentations/10ac0b1d76a6463aa98ad6a9dec917a7/GopherCon_v10.0.pdf

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章