Golang Channel源码解析

文章目录

    • channels
    • How to use Channels
    • Channel源码解析
      • 代码入口
      • channel的结构体
      • 新建channel
      • channel发送元素
      • channel读取元素
      • 关闭channel
      • select channel
        • channel reveive value
        • channel send value
        • 用于判断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

你可能感兴趣的:(golang,go,channel)