GO学习PART1——channel
文章目录
Channel
通道(channel)是Go中的一种特殊的类型,主要用于goroutine之间的消息传递,且channel是一种线程安全的结构
channel基本用法
初始化
// 一般采用make的方法去初始化channel
// 1.构造容量为3的int型channel
make(chan int,3)
// make(chan 数据类型,容量)
// 2.构造任意数据类型的channel
make(chan interface{},3)
// 3.构造没有容量的channel
make(chan struct{})
读操作
ch := make(chan int,3)
// 1.阻塞式接受数据,如果缓存中没有数据可以读,那么goroutine会阻塞
data := <- ch
// 2.非阻塞式接受,不会阻塞,一般是select语句进行使用
data,ok := <- ch
// 3.无视接受的数据,一般这种chan作为信号
<- ch
写操作
ch := make(chan int,3)
// 写入操作,如果缓存中的数据满了,那么会阻塞,直到缓冲中有空的位置,然后写入数据唤醒
ch <- 10
关闭
// 关闭channel
close(ch)
channel源码分析
channel的源码关联在runtime/chan.go,使用的Go SDK版本是1.20
channel数据结构
开篇就是channel的数据结构,存储的chan的数据结构是一种类似于环形数组的方式
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 // 判断channel是否被关闭
elemtype *_type // element type 数据的类型
sendx uint // send index 写数据的起始位置
recvx uint // receive index 读数据的起始位置
recvq waitq // list of recv waiters 读数据的routine阻塞队列
sendq waitq // list of send waiters 写数据的routine阻塞队列
// 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 // 互斥锁,保证读写只有一个goroutine在操作
}
waitq、sudog类型
type waitq struct {
first *sudog //一个双向列表的头指针
last *sudog //一个双向列表的尾指针
}
type sudog struct {
// ...省略
next *sudog //next指针
prev *sudog //prev指针
// ...省略
}
那么对于channel的数据结构其实已经非常的清楚了,他主要就是3个部分
- 互斥锁,保证并发正确性
- 环形数组
- 读、写的阻塞队列
channel读写操作(以读操作为例)
读操作主要调用的函数chanrecv在源码的457行
首先解释block变量,他是对于是否是阻塞式的判断的变量,因为对于非阻塞式读取,不应该把当前线程阻塞,而是应该返回第二个变量参数为false
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// ...
// 如果channel的地址为nil,说明channel没有初始化,那么也不会对他进行读操作
// 首先判断是否是阻塞式,如果不是,那么直接返回值,如果是的话就会把当前协程挂起,抛出异常
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
//...
// 后面操作要对数据进行操作,需要提前加锁
lock(&c.lock)
// 1. channel关闭:如果数组里面没有数据可以读了,那么就返回数据的默认值;如果还有数据就不做任何操作,后面继续读取数据
// 2. channel没有关闭:如果写队列的队列头指针不为空,说明写队列有routine阻塞,这也就说明当前的缓冲区已经满了所以才有协程阻塞,因此会读取数据同时唤醒写协程区写一个数据,同时读取一个
if c.closed != 0 {
if c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// The channel has been closed, but the channel's buffer have data.
} else {
// Just found waiting sender with not closed.
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
}
// 1.缓冲区有数据,那么会通过计算chan的下一个读的位置,将内存数据移动到指定的eq地址,同时recvx++,指向下一个读取位置;随后判断recvx是否越界,越界的话需要重置位置,这也是为什么说它是类环形结构的原因,他只是逻辑上是个环形。读取数据之后需要释放锁,防止死锁,然后返回值
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
// 2.缓冲区没有数据,首先判断是否是阻塞读取,如果是非阻塞读取的话,那么需要直接返回
if !block {
unlock(&c.lock)
return false, false
}
// 后续就是阻塞队列的创建,然后加入阻塞队列,在释放锁之后进行阻塞
// 最后就是在唤醒的时候需要把写入数据进行读取
// ...
}
channel关闭
channel 在关闭的时候需要唤醒所有阻塞的进程
func closechan(c *hchan) {
// 判断当前的chan有没有初始化,不能关闭一个没有初始化的chan
if c == nil {
panic(plainError("close of nil channel"))
}
// 加锁,后续对于数据的操作
// 判断有没有重复的关闭,如果已经关闭,那么抛出异常
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
if raceenabled {
callerpc := getcallerpc()
racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
racerelease(c.raceaddr())
}
// 关闭channel
c.closed = 1
// 用于存储未处理的读协程或者写协程
var glist gList
//后续对于读/写协程的遍历,但是对于channel来说,不可能同时存在既有读协程和写协程,因此相当于下面两个队列只有一个队列存留(因为如果有读协程,那么说明缓冲区的数据是空的,那么写协程就不可能被阻塞,反之亦然)
// release all readers
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
// 对所有的阻塞协程进行goready操作,唤醒
// Ready all Gs now that we've dropped the channel lock.
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}