GoLang之channel数据结构及阻塞、非阻塞操作、多路select

GoLang之channel数据结构及阻塞、非阻塞操作、多路select

1.channel数据结构

type hchan struct {
    qcount   uint           // 数组长度,即已有元素个数
    dataqsiz uint           // 数组容量,即可容纳元素个数
    buf      unsafe.Pointer // 数组地址
    elemsize uint16         // 元素大小
    closed   uint32			// 关闭状态
    elemtype *_type // 元素类型
    sendx    uint   // 下一次写下标位置
    recvx    uint   // 下一次读下标位置
    recvq    waitq  // 读等待队列
    sendq    waitq  // 写等待队列
    lock     mutex  // 锁
}

我们通过make创建一个缓冲区大小为5,元素类型为int的channel。ch是存在于函数栈帧上的一个指针,指向堆上的hchan数据结构。

在这里插入图片描述

因为channel免不了支持协程间并发访问,所以要有一个锁(lock)来保护整个channel数据结构。
对于有缓冲区channel来讲,需要知道缓冲区在哪里(buf),已经存储了多少个元素(qcount),最多存储多少个元素(dataqsize),每个元素占多大空间(elemsize),所以实际上,缓冲区就是一个数组。因为Golang运行时中,内存复制,垃圾回收等机制,依赖数据的类型信息,所以hchan这里还要有一个指针,指向元素类型的类型元数据。此外,channel支持交替的读(接收),写(发送)。需要分别记录读,写 下标的位置,当读和写不能立即完成时,需要能够让当前协程在channel上等待,待到条件满足时,要能够立即唤醒等待的协程,所以要有两个等待队列,分别针对读和写。此外,channel能够close,所以还要记录它的关闭状态,综上所述,channel底层就长这样。

2.channel的阻塞式和非阻塞式操作

2.1发送阻塞

接下来,我们继续使用ch,初始状态下,ch的缓冲区为空,读、写下标都指向下标0的位置,等待队列也都为空。

在这里插入图片描述

然后一个协程g1向ch中发送数据,因为没有协程在等待接收数据,所以元素都被存到缓冲区中,sendx从0开始向后挪,

在这里插入图片描述

第5个元素会放到下标为4的位置,然后sendx重新回到0,此时缓冲区已经没有空闲位置了

在这里插入图片描述

所以接下来发送的第6个元素无处可放,g1会进到ch的发送等待队列中。这是一个sudog类型的链表,里面会记录哪个协程在等待,等待哪个channel,等待发送的数据在哪里,等等消息。

在这里插入图片描述

接下来协程g2从ch接收一个元素,recv指向下个位置,第0个位置就空出来了,

在这里插入图片描述

所以会唤醒sendq中的g1,将elem指向的数据发送给ch,然后缓冲区再次满了,sendq队列为空。

在这里插入图片描述

在这一过程中,可以看到sendx和recvx,都会从0到4再到0,所以channel的缓冲区,被称为"环形"缓冲区。

在这里插入图片描述

如果像这样给channel发送数据,只有在缓冲区还有空闲位置,或者有协程在等着接收数据的时候,才不会发送阻塞

在这里插入图片描述

碰到ch为nil,或者ch没有缓冲区,而且也没有协程等着接收数据,又或者,ch有缓冲区但缓冲区已用尽的情况,都会发送阻塞 。

在这里插入图片描述

2.1解决发送阻塞

那如果不想阻塞的话,就可以使用select,使用select这种写法时,如果检测到ch可以发送数据,就会执行case分支;如果会阻塞,就会执行default分支了。

在这里插入图片描述

2.2接收阻塞

这是发送数据的写法,接收数据的写法要更多一点。第一种写法会将结果丢弃,第二种写法将结果赋给变量v,第三种是comma ok风格的写法,ok为false时表示ch已关闭,此时v是channel元素类型的零值。这几种写法都允许发生阻塞,只有在缓冲区种有数据,或者有协程等着发送数据时 ,才不会阻塞。如果ch为nil,或者ch无缓冲而且没有协程等着发送数据,又或者ch有缓冲但缓冲区无数据时,都会发生阻塞。

在这里插入图片描述

2.3解决接收阻塞

如果无论如何都不想阻塞,同样可以采用非阻塞式写法,这样在检测到ch的recv操作不会阻塞时,就会执行case分支,如果会阻塞,就会执行default分支。

在这里插入图片描述

3.多路select

上面的selec只是针对的单个channel的操作;
多路select指的是存在两个或者更多的case分支,每个分支可以是一个channel的send或recv操作。例如一个协程通过多路select等待ch1和ch2。这里的default分支是可选的。

image-20220915181353456

我们暂且把这个协程记为g1,多路select会被编译器转换为runtime.selectgo函数调用。
第一个参数cas0指向一个数组,数组里装的是select中所有的case分支,顺序是send在前,recv在后。
第二个参数order0指向一个uint16类型的数组,数组大小等于case分支的两倍。实际上被用作两个数组,第一个数组用来对所有channel的轮询进行乱序,第二个数组用来对所有channel的加锁操作进行排序。轮询需要乱序才能保障公平性,而按照固定算法确定加锁顺序才能避免死锁。

在这里插入图片描述

第三个参数pc0和race检测相关,我们暂时不关心。
第四、五个参数nsends和nrecvs分别表示所有case中执行send和recv操作的分支分别有多少个。
第六个参数block表示多路select是否要阻塞等待,对应到代码中,就是有default分支的不会阻塞,没有的会阻塞。

在这里插入图片描述

再来看第一个返回值,它代表最终哪个case分支被执行了,对应到参数cas0数组的下标。但是如果进到default分支则对应-1。第二个返回值用于在执行recv操作的case分支时,表明是实际接收到了一个值,还是因channel关闭而得到了零值。

在这里插入图片描述

再来看第一个返回值,它代表最终哪个case分支被执行了,对应到参数cas0数组的下标。但是如果进到default分支则对应-1。第二个返回值用于在执行recv操作的case分支时,表明是实际接收到了一个值,还是因channel关闭而得到了零值。

在这里插入图片描述

多路select需要进行轮询来确定哪个case分支可操作了,但是轮询前要先加锁,所以selectgo函数执行时,会先按照有序的加锁顺序,对所有channel加锁,然后按照乱序的轮询顺序检查所有channel的等待队列和缓冲区。

在这里插入图片描述

假如检查到ch1时,发现有数据可读,那就直接拷贝数据,进入对应分支。

在这里插入图片描述

假如所有channel都不可操作,就把当前协程添加到所有channel的sendq或recvq中。对应到本例中,g1会被添加到ch1的recvq,以及ch2的sendq中。之后g1会挂起,并解锁所有的channel的锁。

在这里插入图片描述

在这里插入图片描述

假如接下来ch1有数据可读了,g1就会被唤醒,完成对应分支的操作。

完成对应分支的操作后,会再次按照加锁顺序对所有channel加锁,然后从所有sendq或recvq中将自己移除,最后全部解锁,然后返回。

在这里插入图片描述

在这里插入图片描述

这一次我们看到了channel的底层数据结构,了解了环形缓冲区与等待队列,还了解了channel的阻塞与非阻塞式操作,以及多路select的逻辑处理,

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.浅谈channel send操作

虽然channel的读写操作写法众多,但事实上,channel的常规send操作,会被编译器转换为对runtime.chansend1()的调用 ,而它内部只是调用了runtime.chansend()。
非阻塞式(select)的send操作,会被编译器转换为对runtime.selectnbsend()的调用,它也仅仅是调用了runtime.chansend() 。
所以send操作主要是通过runtime.chansend()这个函数实现的。

在这里插入图片描述

5.浅谈channel recv操作

同样的,常规recv操作,会被编译器转换为对runtime.chanrecv1()的调用,而它内部只是调用了runtime.chanrecv(),comma ok风格的写法会被编译器转换为对runtime.chanrecv2()的调用,它的内部也是调用chanrecv() 只不过比chanrecv1()多了一个返回值。
非阻塞式的recv操作,会根据是否为comma ok风格,被编译器转换为对runtime.selectnbrecv(),或者selectnbrecv2()的调用,而它们两个也仅仅是调用了runtime.chanrecv(),所以recv操作主要是通过chanrecv()函数实现的。

在这里插入图片描述

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]中提到了golang中string的数据结构定义,即stringStruct。这个结构体包含了一个指向字符串的指针和字符串的长度。在引用\[2\]中,我们可以看到在字符串生成时,会先构建stringStruct对象,然后再将其转换成string类型。具体的转换过程可以在gostringnocopy函数中找到。该函数接受一个指向字节数组的指针,然后通过构造stringStruct对象,再将其转换成string类型返回。 除了字符串的数据结构操作golang还提供了其他数据结构操作库。例如,对于数组和切片,可以使用内置的函数和方法来进行操作,如append、copy、len和cap等。对于映射(map)和结构体(struct),可以使用点操作符来访问和修改其字段。此外,golang还提供了一些包来处理常见的数据结构,如container包中的堆(heap)和列表(list)等。 总之,golang提供了丰富的数据结构操作库,可以满足不同场景下的需求。通过使用这些库,开发者可以方便地对数据结构进行创建、访问和修改等操作。 #### 引用[.reference_title] - *1* *2* *3* [golang数据结构初探之字符串string](https://blog.csdn.net/dong945221578/article/details/119870727)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值