Golang channel的使用及原理

167 阅读3分钟

Channel是什么

// 不用通过共享内存来通信,而是要通过通信来共享内存;
Do not communicate by sharing memory; instead, share memory by communicating.

Channel就是Go的通信工具,是Go内置的“MQ”,可支持Go的任何内置类型、以及用户自定义的struct、指针等,原生支持并发

特性

在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

Channel用法

初始化

  • 引用类型
ch := make(chan int)
  • 操作符
<- 可用于chan的读写,数据总是从右侧流向左侧
  • chan类型(权限控制)
// 可读可写:chan
ch := make(chan int)
// 只读:<- chan
ch := make(<-chan int)
// 只写:chan <-
ch := make(chan<- int)
  • 大小设置
// 有缓冲:可设置chan大小 
ch := make(chan int, 10)
// 无缓冲:不设置chan大小即为无缓冲
ch := make(chan int)
  • 元素类型
// chan的元素类型:除上述int外,ElementType可以是任何类型
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType

读写

<- ch
ch <- a

阻塞式与非阻塞式

  • 阻塞式
// 读
a := <- ch
for a := range ch {
    ...
}
<- ch
// 写
ch <- a
  • 非阻塞式
// 读
a, ok := <- ch
select {
    case <- ch:
        ...
}
// 写
select {
    case ch <- a:
        ...
}

关闭channel

close(ch)

注意点

  1. 向已关闭的chan写入数据,将会导致panic
close(ch)
ch <- a  // panic: send on closed channel
  1. 关闭chan时会写入一个空值到ch中

Channel实现原理

数据结构

type hchan strcut {
        qcount   uint           // chan中元素数量
	dataqsiz uint           // chan大小
	buf      unsafe.Pointer // buf数组的地址
	elemsize uint16         // chan中元素大小
	closed   uint32         // chan是否关闭
	elemtype *_type         // 元素类型
	sendx    uint           // 写索引
	recvx    uint           // 读索引
	recvq    waitq          // 接收等待者列表
	sendq    waitq          // 发送等待者列表

	lock mutex
}
  • qcount:chan当前的元素数量
  • dataqsiz:chan的容量,即make(chan elemtype, n)里的第二个参数n
  • buf:buf元素数组的地址
  • elemsize:chan里所装的元素的大小
  • sendx:写索引,即当前写到什么位置了
  • recvx:读索引,读到什么位置了
  • recvq:读队列,双向链表
  • sendq:写队列,双向链表
  • lock:chan内部有一把互斥锁,使得同时只能有一个gorotine进行发收,保障了数据的顺序性

chan的buf数组是一个抽象的环形结构,如图:

type sudog struct {
	g *g

	next *sudog
	prev *sudog
	elem unsafe.Pointer // data element (may point to stack)

	acquiretime int64
	releasetime int64
	ticket      uint32

	isSelect bool

	success bool

	parent   *sudog // semaRoot binary tree
	waitlink *sudog // g.waiting list or semaRoot
	waittail *sudog // semaRoot
	c        *hchan // channel
}

sudog是chan的读写队列的元素,它封装了收发元素的Gorotine

初始化Chan

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

	// 参数校验
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
	...

        // 计算需要分配的内存
	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
	// 根据元素大小等设置chan的参数
        ...

        // 初始化chan
	...

        return c
}

创建chan主要分两步,一是参数校验,二是初始化chan结构。

初始化chan结构主要分为三种情况:

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)
  1. 不带buffer的chan或者元素大小为0的chan,只需要分配chan自己的内存空间

  2. 非指针元素,内存可以是连续的,所以分配各元素和chan本身的内存

  3. 指针元素,内存不是连续的,先new一个chan,再单独分配元素buffer的数组

发送数据

发送主要分为以下几个步骤:

  1. chan为nil的情况处理
  2. chan已关闭,直接panic
  3. 消费队列不为空(消费方有需求),直接给他
  4. 消费队列为空(无消费需求),chan没满直接塞进去,chan满了,判断是否阻塞发送

发送给消费者

// 调用send函数
send(c, sg, ep, func() { unlock(&c.lock) }, 3)

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	if sg.elem != nil {
                // 将发送的值直接拷贝到接收值的内存地址
		sendDirect(c.elemtype, sg, ep)
		sg.elem = nil
	}
        // 获取等待接收数据的goroutine
	gp := sg.g
        // 解锁
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
        // 唤醒等待接收数据的goroutine
	goready(gp, skip+1)
}

主要步骤为:

  1. 拷贝发送元素
  2. 获取等待接收数据G
  3. 唤醒等待接收数据的G

发送到chan

// 获取当前发送索引的地址
qp := chanbuf(c, c.sendx)
// 将元素写入到chan队尾
typedmemmove(c.elemtype, qp, ep)
// sendx自增
c.sendx++
// 若发送索引超过队列容量,将发送索引置为0(环形队列)
if c.sendx == c.dataqsiz {
        c.sendx = 0
}
// chan元素数量+1
c.qcount++

主要步骤为:

  1. 获取当前发送索引的地址
  2. 将元素拷贝到chan队尾
  3. sendx+1,元素数量+1

阻塞发送

// 获取当前goroutine
gp := getg()
// 创建一个sudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
        mysg.releasetime = -1
}

// 给sudog赋值,发送元素->elem,gp->g,chan->c
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
// 将sudog和gorotuine绑定
gp.waiting = mysg
gp.param = nil
// 进入写入队列
c.sendq.enqueue(mysg)

// 挂起goroutine,进入等待状态
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)

KeepAlive(ep)

if mysg != gp.waiting {
        throw("G waiting list is corrupted")
}
// 与goroutine、chan解绑,并释放sudog
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
if closed {
        if c.closed == 0 {
                throw("chansend: spurious wakeup")
        }
        panic(plainError("send on closed channel"))
}

主要步骤为:

  1. 获取当前G,创建一个sudog
  2. 将G和SG以及chan绑定,sudog进入写阻塞队列
  3. 挂起goroutine
  4. 被唤醒后解绑,释放sudog

接收数据

主要分为以下几个步骤:

  1. chan为nil的情况处理
  2. chan已关闭,且没有元素,返回false
  3. 发送队列不为空,直接接收
  4. 发送队列为空,chan中有元素则直接读取,chan无元素则阻塞

从发送者接收

recv(c, sg, ep, func() { unlock(&c.lock) }, 3)

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
        // chan无缓冲
	if c.dataqsiz == 0 {
		if ep != nil {
                        // 直接将发送队列中的数据拷贝到接收值
			recvDirect(c.elemtype, sg, ep)
		}
	} else {
                // 有缓冲,而且chan满了
                // 取recv索引地址
		qp := chanbuf(c, c.recvx)
		if ep != nil {
                        // 拷贝recv索引处的值给接收值
			typedmemmove(c.elemtype, ep, qp)
		}
                // 将发送者的值拷贝到recv索引处
		typedmemmove(c.elemtype, qp, sg.elem)
                // recv索引+1
		c.recvx++
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
                // send索引=recv索引,标记队尾
		c.sendx = c.recvx
	}
        // 修改发送者sg状态
	sg.elem = nil
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
        // 唤醒发送者的goroutine
	goready(gp, skip+1)
}

主要步骤为:

  1. 无缓冲的chan直接赋值给接收者
  2. 有缓冲的,拷贝chan中队首的值给接收者,再将发送者的值拷贝到队首,recv索引+1,队首变队尾
  3. 唤醒等待发送数据的G

从chan接收

// 取recvx地址
qp := chanbuf(c, c.recvx)
if ep != nil {
        // 拷贝recvx处的值到接收值
        typedmemmove(c.elemtype, ep, qp)
}
// 清除chan上recvx的值
typedmemclr(c.elemtype, qp)
// recv索引+1
c.recvx++
if c.recvx == c.dataqsiz {
        c.recvx = 0
}
// chan元素数量-1
c.qcount--

主要步骤为:

  1. 拷贝recvx处的值到接收值
  2. 清除chan上recvx的值
  3. recv索引+1,chan元素数量-1

阻塞接收

// 获取当前goroutine
gp := getg()
// 创建sudog
mysg := acquireSudog()
mysg.releasetime = 0

// 绑定sg和g
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// sg进入chan的接收队列
c.recvq.enqueue(mysg)

// 挂起g,阻塞接收
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

if mysg != gp.waiting {
        throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
        blockevent(mysg.releasetime-t0, 2)
}
// 标记sg状态
success := mysg.success
gp.param = nil
mysg.c = nil
// 释放sg
releaseSudog(mysg)

主要步骤为:

  1. 获取当前g,创建sudog,并将sg和g绑定
  2. sg进入chan的接收队列
  3. 挂起g,阻塞接收
  4. 标记sg状态解绑g,释放sg

关闭Chan

func closechan(c *hchan) {
        // 判断chan是否为nil
	if c == nil {
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
        // 已关闭的chan,触发panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("close of closed channel"))
	}

        // 标记close
	c.closed = 1

	var glist gList

	// 释放所有接受者
	for {
		sg := c.recvq.dequeue()
		if sg == nil {
			break
		}
                // 清除接受者的内存
		if sg.elem != nil {
			typedmemclr(c.elemtype, sg.elem)
			sg.elem = nil
		}
                // 将阻塞接受者的goroutine加入到glist
		gp := sg.g
		gp.param = unsafe.Pointer(sg)
		sg.success = false
		glist.push(gp)
	}

	// 释放所有发送者
	for {
		sg := c.sendq.dequeue()
		if sg == nil {
			break
		}
                // 将阻塞发送者的goroutine加入到glist
		sg.elem = nil
		gp := sg.g
		gp.param = unsafe.Pointer(sg)
		sg.success = false
		glist.push(gp)
	}
	unlock(&c.lock)

	// 唤醒所有阻塞的发送者和接受者
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

主要分为以下几个步骤:

  1. chan为nil,则panic
  2. chan已关闭,触发panic
  3. 标记chan的close状态
  4. 释放接受者和发送者,并将阻塞的g加入到glist
  5. 统一唤醒glist中的g,使得g释放