Channel概述
Channel在Go语言(Golang)中是一种用于在Goroutine之间进行通信和同步的机制。Channel提供了一种安全的方式,让多个Goroutine在同步或异步的情况下交换信息,而不需要使用复杂的锁机制。它的主要特点是类型安全——即Channel中只能传递同一种类型的数据。
实现
1、声明与创建:使用make函数创建一个Channel。例如,创建一个可以传递int类型的Channel:
ch := make(chan int) //无缓冲区
ch := make(chan int ,3) //有缓冲区,容量为3
2、发送数据:使用<-操作符向Channel发送数据。例如:
ch <- 10
3、接收数据:使用<-操作符从Channel接收数据。例如:
value := <-ch
有无缓冲区的区别(特性)
首先我们看一下channel的代码实现(go/src/runtime/chan.go):
type hchan struct {
//channel分为无缓冲和有缓冲两种。
//对于有缓冲的channel存储数据,借助的是如下循环数组的结构
qcount uint // 循环数组中的元素数量
dataqsiz uint // 循环数组的长度
buf unsafe.Pointer // 指向底层循环数组的指针 //驿站货架
elemsize uint16 //能够收发元素的大小
closed uint32 //channel是否关闭的标志
elemtype *_type //channel中的元素类型
//有缓冲channel内的缓冲数组会被作为一个“环型”来使用。
//当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
sendx uint // 已发送元素在循环数组中的索引
recvx uint // 已接收元素在循环数组中的索引
//当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列
//当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列
recvq waitq // 读等待队列 //取货的人排的队
sendq waitq // 写等待队列 //进站的货物排的队
lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}
可以看出hchan结构体的主要组成部分有四个:
- buf:用来保存goroutine之间传递数据的循环链表(实际数据结构是用数组实现的)。
- sendx和recvx:用来记录此循环链表当前发送或接收数据的下标值。
- sendq 和 recvq:用于保存向该chan发送和从改chan接收数据的goroutine的队列。
- lock:保证channel写入和读取数据时线程安全的锁。
其中无缓冲区的channel只用得上sendq、recvq、lock,有缓冲区的channel全部需要。
这时就可以看到有缓冲区和无缓冲区的channel的一些区别了:
- 有缓冲区的channel和无缓冲区的channel最大的区别就是有无缓冲区(
狗头), 缓冲区的作用是记录当前channel中还存在的数据。当缓冲区充足的时候,channel的读写都不会阻塞(具体为当缓冲区不为0时,读操作不会阻塞;当缓冲区不为满时,写操作不会阻塞。) - 有缓冲区的channel是异步的。无缓冲区的channel是同步的。可以理解为缓冲区是一个面包货架队列,当货架(缓冲区)没有装满时,厨师做好的面包(数据)会存放到货架,当货架满时,需要等到最前面面包被取走了,留出了空位再存放进货架。而无缓冲区的channel是一个尽职的面包外卖员,必须要将手上的面包交给某个人后,再拿着后续的面包给其他人。
根据面包理论(我编的 ,有错误欢迎指正)
某channel是一个面包店长,有缓冲区的channel有货架(channel),可以临时存放一些面包(数据)。同时会存在着很多的厨师,每个厨师(发送的channel)都会单独给他做面包(发送数据),而消费者(接收的channel)则会从面包店长那买面包(接收数据)。
没有缓冲区的面包店长就比较惨了,他没有货架。厨师(发送的channel)给他做好了一个面包,他就得充当外卖员,去把这个面包送到消费者(接收的channel)手上。
可以发现存在着一些问题,这些问题也是channel会引发错误的问题。
- 给一个 nil channel 发送数据,造成永远阻塞。货架或者外卖员存在,面包要给不存在的消费者送过去,即等待不存在的人来收货,就会一直等下去(阻塞)。
- 从一个 nil channel 接收数据,造成永远阻塞。店面的货架和外卖员都不存在,消费者要等待不存在的人来送货,也会一直等下去(阻塞)。
- 给一个已经关闭的 channel 发送数据,引起 panic。货架被撤了或者外卖员被裁了,厨师给他们新的面包,他们会很不乐意。所以会报错。
- 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值。面包店倒闭了,只会把库存的面包一件件卖完(返回数据和读取成功标识),然后告诉你我现在什么都没有了,跑路了(返回零值和读取失败标识)。外卖员被裁了,但因为他很有责任心,所以会把最后一件面包送到客户手上(返回数据和读取成功标识),然后再有人找他拿面包就会听到他说我被裁了(返回零值和读取失败标识)。
channel 是否线程安全?
是线程安全的,根据hchan的数据结构可以看出来,channel的使用是有互斥锁的。保证了读写操作的原子性。
channel读写流程
读取channel
依旧是面包理论(你现在是消费者,你想要去买个面包):
- 消费者(读取channel的channel)去买面包(数据),如果发现面包店排队了(recvq的长度大于零),那消费者需要老老实实跟在后面排队(进入recvq,然后休眠)。然后等店长叫你(唤醒)。
- 买面包的时候发现没有排队,并且货架上有面包,店长会把做的最早的面包给你(缓冲区头的数据)
- 买面包的时候发现没有排队,但是货架上没有面包了,你要成为排队的第一个人(进入recvq,然后休眠),等店长叫你。
- 拿到面包后,结束读取
换成实际的流程就是:
- 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
- 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;
写入channel
面包理论(现在你是厨师辣,你现在刚做好了一个面包):
- 厨师(写入该channel的channel)做好了一个面包(准备发送数据),首先你看了一下有没有排队的人(recvq的长度大于0),如果有的话你把你做好的面包给店长,店长会叫醒排队的第一个人(唤醒等待的channel),然后把面包直接给他。
- 如果没有排队的人,你就会看看货架,如果货架没有满,你就把你的面包放到了货架的最后的面包的后面(存入缓冲区)
- 如果货架满了,你就需要去排厨师的队了(存入sendq,睡眠),等到店长提醒你货架有空位(唤醒)。
换成实际的流程就是:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;
528

被折叠的 条评论
为什么被折叠?



