chan数据结构
前言
channel是Golang提供goroutine的通信方式,channel主要用于进程内各goroutine间通信。:
src/runtime/chan.go:hchan 定义了channel的数据结构:
qcount:当前队列中的剩余的元素个数
dataqsiz:当前队列的长度,即可以存放的元素个数
buf:用来保存goroutine之间传递数据的循环链表
elemsize:每个元素的大小
closed:标识关闭状态
elementtype:元素类型
sendx:元素写入时存放队列中的位置
recvx:队列读出元素的位置
recvq:等待读消息的goroutine队列
sendq:等待写消息的goroutine队列
lock:保证channel写入和读取数据时线程安全的互斥锁
总结hchan结构体的主要组成部分有四个:
用来保存goroutine之间传递数据的循环链表。=> buf。
用来记录此循环链表当前发送或接收数据的下标值。=> sendx和recvx。
用于保存向该chan发送和从改chan接收数据的goroutine的队列。=====> sendq 和 recvq
保证channel写入和读取数据时线程安全的锁。 =====> lock
举个栗子
/G1
func sendTask(taskList []Task) {
...
ch:=make(chan Task, 4) // 初始化长度为4的channel
for _,task:=range taskList {
ch <- task //发送任务到channel
}
...
}
//G2
func handleTask(ch chan Task) {
for {
task:= <-ch //接收任务
process(task) //处理任务
}
}
ch是长度为4的带缓冲的channel,G1是发送者,G2是接收者
初始hchan结构体重的buf为空,sendx和recvx均为0。
当G1向ch里发送数据时,首先会对buf加锁,然后将数据copy到buf中,然后sendx++,然后释放对buf的锁。
当G2消费ch的时候,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁。
可以发现整个过程,G1和G2没有共享的内存,底层是通过hchan结构体的buf,并使用copy内存的方式进行通信,最后达到了共享内存的目的,这里也体现了Go中的CSP并发模型。
底层实现:环形队列
chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个元素的channel示意图:
等待队列
从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中:
- 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
- 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;
注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据。
一个channel只能传递一个类型的值,仅允许被同一个goroutine读写(锁)
创建channel
创建channel的过程实际上就是对hchan初始化的过程,类型信息和缓冲区长度由remake传入,buf的大小则与元素大小和缓冲区长度共同决定
向channel写数据
向一个channel中写数据简单过程如下:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;(G->goroutine)
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq(接收队列),进入睡眠,等待被读goroutine唤醒;
从channel读数据
从一个channel读数据简单过程如下:
- 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
- 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;
关闭channel
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。
除此之外,panic出现的常见场景还有:
- 关闭值为nil的channel
- 关闭已经被关闭的channel
- 向已经关闭的channel写数据
单项channel
实际上就是通过参数传递来控制的
使用select可以监控多channel,比如监控多个channel,当其中某一个channel有数据时,就从其读出数据。
一个简单的示例程序如下:
package main
import (
"fmt"
"time"
)
func addChannel(chanName chan int){
for {
chanName <- 1//写入数据
time.Sleep(1 * time.Second)
}
}
func main() {
var chan1 = make(chan int,10) //channel的元素类型为Int,channel的容量大小为10
var chan2 = make(chan int ,10)
go addChannel(chan1)
go addChannel(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)
}
}
}
输出结果
No element in chan1 and chan2.
Get element from chan2: 1
Get element from chan1: 1
Get element from chan1: 1
Get element from chan2: 1
No element in chan1 and chan2.
No element in chan1 and chan2.
Get element from chan1: 1
Get element from chan1: 1
Get element from chan2: 1
Get element from chan2: 1
No element in chan1 and chan2.
No element in chan1 and chan2.
Get element from chan1: 1
Get element from chan2: 1
No element in chan1 and chan2.
Get element from chan2: 1
Get element from chan2: 1
Get element from chan1: 1
Get element from chan1: 1
No element in chan1 and chan2.
Get element from chan1: 1
No element in chan1 and chan2.
Get element from chan2: 1
....
从输出可见,从channel中读出数据的顺序是随机的,事实上select语句的多个case执行顺序是随机的
通过这个示例想说的是:select的case语句读channel不会阻塞,尽管channel中没有数据。这是由于case语句编译后调用读channel时会明确传入不阻塞的参数,此时读不到数据时不会将当前goroutine加入到等待队列,而是直接返回。
range
通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。
注意:如果向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞。
package main
import (
"fmt"
"time"
)
func addChannel(chanName chan int){
for {
chanName <- 1//写入数据
time.Sleep(1 * time.Second)
}
}
func chanRange (chanName chan int){
for e := range chanName{
fmt.Printf("Get element from chan: %d\n", e)
}
}
func main() {
var chan1 = make(chan int,10) //channel的元素类型为Int,channel的容量大小为10
//var chan2 = make(chan int ,10)
go addChannel(chan1)
chanRange(chan1)
//go addChannel(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)
//
// }
//}
}
输出
Get element from chan: 1
Get element from chan: 1
Get element from chan: 1
Get element from chan: 1
....
推荐文章:https://juejin.cn/post/7037656471210819614