go语言常用数据结构之 channel

channel 是 go 在语言层面提供的 goroutine 间的通信方式, 比 unix 管道更易用也更轻便。

主要用于进程内各个 goroutine 间通信。

基于以上的需求, chan 是用于协程间通信的, 那么他肯定需要有一个东西来存储协程间需要传递的数据。

而这个传递数据的地方需要能指定大小,然后保证先进先出。

另外, 他需要记住是哪个协程要跟哪个协程进行通信,

而且要考虑如果有多个携程怎么处理(并发)。

chan 数据结构 :

src/runtime/chan.go:hchan定义了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     mutex          // 互斥锁,chan不允许并发读写
}

从上面的数据结构也能看出来, chan 主要包含四大部分 :

1、 qcount、 dataqsiz、 buf、 sendx、 recvq 这5个东西用来描述 chan 中的队列,保证数据的读写

2、elemsize、 elemtype 这两个用来描述 chan 的能传递的数据类型

3、recvq 、 sendq 用来保存和管理需要往 chan 读写数据的各个 goroutine 。

4、 lock 是互斥锁, 用来解决并发问题, closed 是状态标识符

一、环形队列

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

下图展示了一个可缓存6个元素的channel示意图:

dataqsiz指示了队列长度为6,即可缓存6个元素;

buf指向队列的内存,队列中还剩余两个元素;

qcount表示队列中还有两个元素;

sendx指示后续写入的数据存储的位置,取值[0, 6);

recvx指示从该位置读取数据, 取值[0, 6);

二、等待队列

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

然后挂起在recvq这个协程等待队列中,等待往channel写数据的 goroutine 唤醒。

往 channel 写数据, 如果 channel 缓冲区以满或者无缓冲区,则当前 goroutine 会被阻塞,

然后挂起在sendq这个协程等待队列中,等待向channel读数据的 goroutine 唤醒。

图展示了一个没有缓冲区的channel,有几个goroutine阻塞等待读数据:

一般情况下recvq和sendq至少有一个为空。

只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据。

三、类型信息

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

elemtype代表类型,用于数据传递过程中的赋值;

elemsize代表类型大小,用于在buf中定位元素位置。

四、锁和 closed 状态码

一个channel同时仅允许被一个goroutine读写,

closed 用于决定 chan 是否还可写 和 重置(还是清空?)chan中的数据。

channel 读写

一、创建 channel

基于上面的需求和数据结构,我们可以看到,其实make创建 chan 就是初始化一个 hchan 结构体, 然后返回指针

func makeChan(t *chantype, size int) *hchan {
    return &hchan{
        buf:malloc(元素类型大小*size),
        elemsize:元素类型大小,
        elemtype:元素类型,
        dataqsiz:size,
    }
}

二、向 channel 中写数据

1、如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,

此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;

2、如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;

3、如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;

流程图如下 :

三、从 channel 读取数据

1、如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;

2、如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,

把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;

3、如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;

4、将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;

流程图如下 :

四、关闭channel

关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。

把sendq中的G全部唤醒,但这些G会panic。

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

关闭值为nil的channel

关闭已经被关闭的channel

向已经关闭的channel写数据

五、常见用法

select :

使用select可以监控多channel,比如监控多个channel,当其中某一个channel有数据时,就从其读出数据。

一个简单的示例程序如下:

package main

import (
	"fmt"
	"time"
)

func addNumberToChan(chanName chan<- int) {
	for {
		chanName <- 1
		time.Sleep(1 * time.Second)
	}
}

func main() {

	var chan1 = make(chan int, 10)
	var chan2 = make(chan int, 10)

	go addNumberToChan(chan1)
	go addNumberToChan(chan2)

	for {
		select {
		case e := <-chan1:
			fmt.Printf("Get element from chan1: %d\n", e)
		case e := <-chan2:
			fmt.Printf("Get element from chan2: %d\n", e)
		default:
			fmt.Printf("No element in chan1 and chan2.\n")
			time.Sleep(1 * time.Second)
		}
	}
}

程序中创建两个channel: chan1和chan2。函数addNumberToChan()函数会向两个channel中周期性写入数据。

通过select可以监控两个channel,任意一个可读时就从其中读出数据。

从channel中读出数据的顺序是随机的,事实上select语句的多个case执行顺序是随机的

select的case语句读channel不会阻塞,尽管channel中没有数据。这是由于case语句编译后调用读channel时会明确传入不阻塞的参数,此时读不到数据时不会将当前goroutine加入到等待队列,而是直接返回。

range

通过range可以持续从channel中读出数据,好像在遍历一个数组一样,

当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。

func chanRange(chanName chan int) {
	for e := range chanName {
		fmt.Printf("Get element from chan: %d\n", e)
	}
}

注意:如果向此channel写数据的goroutine退出时,

系统检测到这种情况后会panic,否则range将会永久阻塞。

参考 : https://www.toutiao.com/i6862487725497876995

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值