go channel底层实现原理分享

200 阅读5分钟

我正在参加「掘金·启航计划」

1、前言

channel是在语言层面提供的goroutine间的通信方式,channel主要用于进程内各goroutine间的通信,如果需要跨进程通信,建议使用分布式系统方法解决。

2、chan数据结构

channel由队列、类型信息、goroutine等待队列组成。

src/runtime/chan.go包中定义了channel的数据结构:

type hchan struct {
	qcount   uint           // 当前队列中剩余元素个数
	dataqsiz uint           // 环形队列长度,即可以存放的元素个数
	buf      unsafe.Pointer // 环形队列指针
	elemsize uint16			//每个元素的大小
	closed   uint32			//标识关闭状态
	elemtype *_type // 元素类型
	sendx    uint   // 队列下标,指示元素写入时存放到队列中的位置
	recvx    uint   // 队列下标,指示元素从队列的该位置读出
	recvq    waitq  // 等待读消息goroutine队列
	sendq    waitq  // 等待写消息的goroutine队列

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex	//互斥锁,chan不允许并发读写
}

2.1、环形队列

chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。

  • dataqsiz 指示了队列长度为6;
  • buf指向队列的内存,队列中还剩余两个元素;
  • qcount表示队列中还有两个元素;
  • sendx指示后续写入的数据存储的位置,取值[0,6)
  • recvx指示从该位置读取数据,取值[0, 6)

2.2、等待队列

从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。

向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。

被阻塞的goroutine将会挂在channel的等待队列中:

  • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
  • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;

示例:

一个没有缓冲区的channel,有几个goroutine阻塞等待读数据。

2.3、类型信息

一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。

  • elemtype代表类型,用于数据传递过程的赋值;
  • elemsize代表类型大小,用于在buf中定位元素位置;

2.4、锁

一个channel同时仅被一个goroutine读写。

3、channel读写

3.1、创建channel

创建channel实际上是初始化hchan结构,其中类型信息和缓冲区长度由make语句传入,buf的大小与元素大小和缓冲区长度决定。

func makechan(t *chantype, size int) *hchan {
    var c *hchan
    c = new(hchan)
    c.buf = malloc(元素类型大小*size)
    c.elemsize = uint16(elem.size) //元素类型大小
    c.elemtype = elem   //元素类型
    c.dataqsiz = uint(size)
    lockInit(&c.lock, lockRankHchan)
    return c
}

3.2、向channel写数据

向一个channel中写数据流程如下:

  1. 写入数据的时候,若recvq 队列为空,且循环队列有空位,那么就直接将数据写入到 循环队列的队尾 即可
  2. 若recvq 队列为空,且循环队列无空位,则将当前的协程放到sendq等待队列中进行阻塞,等待被唤醒,当被唤醒的时候,需要写入的数据,已经被读取出来,且已经完成了写入操作
  3. 若recvq 队列为不为空,那么可以说明循环队列中没有数据,或者循环队列是空的,即没有缓冲区(向无缓冲的通道写入数据),此时,直接将recvq等待队列中取出一个G,写入数据,唤醒G,完成写入操作

3.3、从channel读数据

从一个channel读数据简单过程如下:

  1. 若sendq为空,且循环队列无元素的时候,那就将当前的协程加入recvq等待队列,把recvq等待队列对头的一个协程取出来,唤醒,读取数据
  2. 若sendq为空,且循环队列有元素的时候,直接读取循环队列中的数据即可
  3. 若sendq有数据,且循环队列有元素的时候,直接读取循环队列中的数据即可,且把sendq队列取一个G放到循环队列中,进行补充
  4. 若sendq有数据,且循环队列无元素的时候,则从sendq取出一个G,并且唤醒他,进行数据读取操作

3.4、关闭channel

关闭channel时会把recvq的G全部唤醒,本该写入G的数据为nil,把sendq中的G全部唤醒,但这些G会panic。

func closechan(c *hchan) {
	if c == nil {
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
	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())
	}

	c.closed = 1

	var glist gList

	// release all readers
	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)
	}

	// release all writers (they will panic)
	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)
	}
	unlock(&c.lock)

	// Ready all Gs now that we've dropped the channel lock.
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

除此之外,出现panic的场景还有:

  • 关闭值为nil的channel
  • 关闭已经被关闭的channel
  • 向已经关闭的channel写数据

4、常见用法

4.1、select

GO 里面Chan 一般会和 select 搭配使用,直接上代码

package main

import (
	"fmt"
	"time"
)

func main() {
	//创建两个int类型的通道,缓冲区为10
	chan1 := make(chan int, 10)
	chan2 := make(chan int, 10)

	//向chan1写入数据
	go func() {
		i := 0
		for {
			chan1 <- i
			i++
			time.Sleep(time.Second)
		}
	}()
	//向chan2写入数据
	go func() {
		i := 0
		for {
			chan2 <- i
			i++
			time.Sleep(time.Second)
		}
	}()
	//读取数据
	for {
		select {
		case num := <-chan1:
			fmt.Printf("chan1读取数据:%d\n", num)
		case num := <-chan2:
			fmt.Printf("chan2读取数据:%d\n", num)
		default:
			fmt.Println("未读取到数据")
			time.Sleep(time.Second)
		}
	}
}

从运行结果来看,select 监控的 2个 通道,读取到的数据是随机的