初窥:golang 中 channel 实现与使用

1. channel 概述

channel 是golang中的一种数据结构,主要用来实现golang协程之间的通信,channel的设计也是提现了golang对于进程间消息传递的观点:“用通信实现共享内存,而不是使用共享内存实现通信”。

2. channel 数据结构

在golang源码中定义了一个结构体:hchan,这个结构体就是channel的数据结构

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 mutex
}

2.1. 各个字段的含义以及作用

  • buf:一个指针,指向一个数组,这个数组作为一个缓存区用来存储送入channel元素,通过sendxrecvx,数组实现环形队列
  • qcount:缓存区存放的元素个数
  • dataqsize:缓冲区数组长度
  • elemsize:元素大小 (存疑
  • closed:指示channel状态
  • elemtype:元素类型
  • sendx:环形队列队尾,用于插入元素
  • recvx:环形队列队首,用于弹出元素
  • recvq:接收者队列,是一个双向链表,无法从channel取到值的goroutine将被加入这个等待队列,同时该goroutine进入阻塞
  • sendq:发送者队列,是一个双向链表,无法将值推入channel的goroutine将被加入这个等待队列,同时该goroutine进入阻塞
  • lock:锁,实现channel的并发安全

比较疑惑,不知道elemsize字段意义何在,假如我用channel传递一个结构体,结构体内有一个slice,那结构体长度是可变的,那elemsize要记录什么?我在想是不是对于一些存在变长可能的数据结构,放进区channel里面的时候实际上放的是该结构实体的指针?弹出去的时候再恢复成实体

recvq与sendq的结构:waitq

type waitq struct {    
	first *sudog    
	last  *sudog
}

type sudog struct {    
	// Current goroutine    
	g *g    
	// isSelect indicates g is participating in a select, so    
	// g.selectDone must be CAS'd to win the wake-up race.    
	isSelect bool    
	next     *sudog    
	prev     *sudog    
	elem     unsafe.Pointer // data element (may point to stack)    
	// The following fields are never accessed concurrently.    
	// For channels, waitlink is only accessed by g.    
	// For semaphores, all fields (including the ones above)    
	// are only accessed when holding a semaRoot lock.    
	acquiretime int64    
	releasetime int64    
	ticket      uint32    
	parent      *sudog // semaRoot binary tree    
	waitlink    *sudog // g.waiting list or semaRoot    
	waittail    *sudog // semaRoot   
	c           *hchan // channel
}

不是很懂这些字段和作用…

2.2. channel工作过程:读与写

Created with Raphaël 2.3.0 发起写入操作 recvq 非空? 从recvq弹出一个读者G 数据写入读者G 唤醒读者G 写入完成 qcount
Created with Raphaël 2.3.0 发起读操作 sendq 非空? 从sendq取出一个写入者G buf==nil ? 读取写入者G数据 唤醒写入者G 读完成 从buf队首取一个元素 将写入者G的数据写入buf qcount>0? 从buf读取一个元素 将读取者G加入recvq 读者G等待被唤醒 读者G被唤醒 yes no yes no yes no

2.3. 操作 channel 后:成功、阻塞或者panic

  • 读取关闭channel
    • channel 非空:返回数据和读取状态true
    • channel空:返回类型零值和false
  • 写入关闭channelpanic
  • 读取非关闭channel
    • channel空:阻塞
    • channel非空:返回数据和读取状态true
  • 写入非关闭channel
    • channel 满:阻塞
    • channel 未满:写入成功

2.4. range 与 select

range
利用range可以不断从channel里面读取值,当通道关闭时,利用读取状态来决定是否退出range

c := make(chan int, 10)
go func(){
	for i:=0;i<5;i++{
		c<-i
	}
	close(c)
}
for v,ok := range c {
	if !ok{
		fmt.Println(v,ok)
		break
	}
	fmt.Println(v,ok)
}
// 输出
0 true
1 true
2 true
3 true
4 true
0 false

select
利用select可以不断操作多个channel,通常也被用来通知一个goroutine关闭

在标准库context中,Context类型的 Done()函数会返回一个Donechannel,WithCancel()函数会需要一个Context类型的对象,然后返回该对象的一个克隆体和一个CancalFunc类型的函数cancel,这个克隆体的Done有别于其父本的Done,当其父本Done关闭或者cancel被调用时,这个Done会被关闭。

func main() {
	gen := func(ctx context.Context) <-chan int {
		dst := make(chan int)  // 无缓冲通道
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done():
					return
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	for v := range gen(ctx) {
		fmt.Println(v)
		if v == 5 {
			break
		}
	}
}
// 输出
1
2
3
4
5

2.5. channel 缓冲:有与无

有缓冲
有缓冲通道可以让goroutine之间的信息传递更加高效,减少不必要的阻塞,通过 2.3 小节我们知道

  • 读一个非空的有缓冲通道不会阻塞
  • 写一个非满的有缓冲通道不会阻塞
c := make(chan int, 10)  // 定义一个最多可以缓存10个元素的带缓冲通道

无缓冲
无缓冲通常用来同步两个goroutine,对无缓冲通道的所有读(写)操作都将阻塞直到有写(读)操作发生

c := make(chan int)  // 定义一个不带缓冲的通道

你可能感兴趣的:(Go基础,go)