Contents

go channel

channel

channel被设计用来goroutine之间通讯的。go在共享内存的设计理念是:不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存。

数据结构与原理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 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
}
字段 含义
buf 指向缓存区的指针
qcount 缓存区已有的元素个数
dataqsize 缓存区数组容量
elemsize 元素大小
elemtype 元素类型
sendx ch<-x ,goroutine发送数据到缓存区,填充数据的下标,写操作
recvx x:=<-ch,goroutine向channel读数据的下标,读操作
recvq 消费者阻塞队列
sendq 生产者阻塞队列
lock 锁,用于维持缓存区数据的完整性
closed c.closed != 0 表示channel已关闭

channel分为有缓存和无缓存两种。对于有缓存来说,需要缓存区来存储数据,缓存区是一个环形数组,sendx,recvx下标超过或等于数组长度会被置成0。

1
2
3
4
5
6
7
8
// /usr/local/go/src/runtime/chan.go 
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  		.....
		if c.recvx == c.dataqsiz {
			 c.recvx = 0
		}
  		.....
}

有缓存channel

img

当有goroutinechannel发送数据时

img

如果这时候再向channel发送一个数据,recvx=sendx表示为缓存区为空或者已满

而判断channel的缓存区是否已满,源码是这样判断的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// /usr/local/go/src/runtime/chan.go 
func full(c *hchan) bool {
	// c.dataqsiz is immutable (never written after the channel is created)
	// so it is safe to read at any time during channel operation.
	if c.dataqsiz == 0 {
		// Assumes that a pointer read is relaxed-atomic.
		return c.recvq.first == nil
	}
	// Assumes that a uint read is relaxed-atomic.
	return c.qcount == c.dataqsiz
}

至于缓存区满了之后需不需要阻塞是由block参数决定的

1
2
3
4
5
6
7
8
// /usr/local/go/src/runtime/chan.go 
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
		....
		if !block && c.closed == 0 && full(c) {
				return false
		}
		....
}

怎么样才能让channel无阻塞呢,需要select语句中default关键字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
	ch := make(chan int, 10)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
		select {
		case ch <- 1:
		default:
			fmt.Println("no block")
		}
	}()

	time.Sleep(time.Minute)
}

所谓的阻塞,是阻塞当前goroutine,不管是从channel中取数据,还是从channel发送数据,都有可能阻塞。所以channel的数据结构里面有两个等待队列:recvq,sendq。用来记录阻塞的goroutine。等待队列是sudog类型的双向列表,sudog记录了哪个goroutine在等待,等待哪个channel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// /usr/local/go/src/runtime/chan.go 
type waitq struct {
	first *sudog
	last  *sudog
}

type sudog struct {

	g *g

	next *sudog
	prev *sudog
	elem unsafe.Pointer


	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
}

当满足条件时,要立刻唤醒等待的goroutine

字段 含义
g 等待的goroutine
elem 元素的地址

当有goroutine读走一个数据的时候:v:=<-ch

img

无缓冲channel

这些字段都是零值

字段
buf nil
qcount 0
dataqsize 0
sendx 0
recvx 0

工作的只有recvq,sendq,当有goroutine发送数据时,会直接把数据指针赋值给recvq的第一个元素,如果recvq为空,阻塞当前goroutine,并加入到sendq;当有goroutine接收数据时,会直接从sendq中取出队首元素的数据

img

无数据channel

有时候遇到的场景是,goroutine之间的通讯不涉及数据,但是需要有触发时机。这个时候,channel可以定义无数据的channel :ch:=make(chan struct{},5),此时buf字段是nil,其他的字段正常