在介绍底层原理之前,我们先简单地介绍一下channel的使用方法,比如初始化,发送和接收消息,然后我们在分别介绍他们在底层是如何实现的。
使用channel的常见使用方法如下。
- 申明channel
var stop chan int
此时stop为nil
- 初始化channel
stop = make(chan int, 1)
- 向channel发送消息
stop <- 10
- 从channel接收消息
msg := <- stop
- 关闭channel
close(stop)
- select 多路复用
a := make(chan int)
b := make(chan string)
select {
case msg := <- a:
fmt.Printf("recv: %d\n", msg)
case b <- 10:
fmt.Print("send \n")
default:
fmt.Print("default")
}
channel底层结构 上面我们简单回顾了channel的基本使用方法,我并没有说太多如何使用,比如buffer channel和no buffer channel的区别,还有阻塞等,相信读者基本都已经掌握了channel的使用,亟需一些对channel的深入理解。
我常常见到一些golang新手,在使用channel的时候会这样写,这有什么问题呢?
type notify struct {
a int
b string
c bool
d []int
}
var stop = make(chan notify, 10000)
我们先来看看底层结构,就知道这样写初始化有多离谱。channel在运行时是hchan,定义如下。
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 protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
我们先以下图来描述一下channel,然后我们就能了解每个字段是用来做什么的,相信这样可以更好地帮助读者理解。
可以看出,channel的结构是由一个环形队列buf来存储send到channel的数据。使用双向链表recvq和sendq来存储接收阻塞和发送阻塞的goroutine。
- qcount 环形队列中的数据个数
- dataqsize 环形队列的长度
- buf 指向环形队列的指针
- sendx 向该通道发送数据的时候,存储在环形队列的什么位置
- recvx 从该通道接收数据的时候,从环形队列的何处开始读取。
- elemsize channel中消息的大小
- elemtype channel中消息的类型
- recvq 正在读取消息的阻塞协程队列
- sendq 正在发送消息的阻塞协程队列
- lock 锁,用于并发保护,比如读写环形队列。
- 用Go实现一个环形队列(buf所指向的即为环形队列)
- 用Go实现一个双向链表(sendq和recvq的类型waiq就是一个双向链表的实现)
channel源代码位置:
本篇就暂时先说到这里,我们对channel就有了个大概的理解,下一篇我们继续讲解channel的初始化,写数据,读数据以及对应的协程唤醒或者阻塞等原理。关注我,不走丢哦