GO 语言中 chan 的理解
GO 语言中 chan 的理解
chan 的底层实现是怎么样的?
chan 是 Go 语言中的一个关键字,用于实现并发通信。chan 可以用于在不同的 goroutine 之间传递数据,实现数据的同步和异步传输。
在底层实现上,chan 是通过一个结构体来表示的,这个结构体包含了一个指向数据的指针和两个指向信道的指针。其中,一个指针用于发送数据,另一个指针用于接收数据。
下面是 chan 的底层实现代码:
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 // 锁
}
chan 的发送和接收操作的底现
当我们向 chan 发送数据时,会先检查 chan 是否已经关闭。如果 chan 已经关闭,那么发送操作会直接返回一个 panic。否则,会将数据复制到队列中,并更新发送位置。
下面是 chan 发送操作的底层实现代码:
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
// 检查 chan 是否已经关闭
if c.closed != 0 {
panic("send on closed channel")
}
// 计算发送位置
i := c.sendx
// 计算队列中的元素数量
if c.qcount < c.dataqsiz {
c.qcount++
} else {
// 如果队列已满,需要扩容
grow(c)
}
// 更新发送位置
c.sendx++
// 将数据复制到队列中
qput(c, i, ep)
return true
}
当我们从 chan 接收数据时,也会先检查 chan 是否已经关闭。如果 chan 已经关闭并且队列中没有数据,那么接收操作会直接返回一个零值。否则,会从队列中取出数据,并更新接收位置。
下面是 chan 接收操作的底层实现代码:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 检查 chan 是否已经关闭
if c.closed != 0 && c.qcount == 0 {
return false, false
}
// 计算接收位置
i := c.recvx
// 如果队列中没有数据,需要阻塞等待
for c.qcount <= 0 {
if !block {
return false, false
}
gopark(chanparkcommit, unsafe.Pointer(c), "chan receive", traceEvGoBlockRecv, 1)
}
// 从队列中取出数据
qget(c, i, ep)
// 更新接收位置
c.recvx++
// 更新队列中的元素数量
c.qcount--
return true, true
}
chan 是如何实现多个 gorouting 并发安全访问的?
如上 hchan 结构中的 recvq 和 sendq 分别表示接收等待队列和发送等待队列,它们的定义如下:
type waitq struct {
first *sudog // 等待队列的第一个元素
last *sudog // 等待队列的最后一个元素
}
sudog 表示等待队列中的一个元素,它的定义如下:
type sudog struct {
// 等待的 goroutine
g *g
// 是否是 select 操作
isSelect bool
// 等待队列中的下一个元素
next *sudog
// 等待队列中的上一个元素
prev *sudog
// 等待的元素
elem unsafe.Pointer
// 获取锁的时间
acquiretime int64
// 保留字段
release2 uint32
// 等待的 ticket
ticket uint32
// 父 sudog
parent *sudog
// 等待链表
waitlink *sudog
// 等待链表的尾部
waittail *sudog
// 关联的 chan
c *hchan
// 唤醒时间
releasetime int64
}
当 chan 的队列已满或为空时,当前 goroutine 会被加入到发送等待队列或接收等待队列中,并释放锁。当另一个 goroutine 从 chan 中取出数据或向 chan 发送数据时,它会重新获取锁,并从等待队列中取出一个 goroutine,将其唤醒。这样,多个 goroutine 就可以通过等待队列来实现并发访问 chan。
sudog 是 Go 中非常重要的数据结构,因为 g 与同步对象关系是多对多的。
一个 g 可以出现在许多等待队列上,因此一个 g 可能有很多sudog:在 select 操作中,一个 goroutine 可以等待多个 chan 中的任意一个就绪, sudog 中的 isSelect 字段被用来标记它是否是 select 操作。当一个 chan 就绪时,它会唤醒对应的 sudog,并将其从等待队列中移除。如果一个 sudog 是 select 操作,它会在唤醒后返回一个特殊的值,表示哪个 chan 就绪了
多个 g 可能正在等待同一个同步对象,因此一个对象可能有许多 sudog:chan 在不同的 gorouting 中传递等待
完整的发送和接受方法实现如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
// 获取 chan 的锁
lock(&c.lock)
// 检查 chan 是否已经关闭
if c.closed != 0 {
unlock(&c.lock)
panic("send on closed channel")
}
// 计算发送位置
i := c.sendx
// 计算队列中的元素数量
if c.qcount < c.dataqsiz {
c.qcount++
} else {
// 如果队列已满,需要将当前 goroutine 加入到发送等待队列中
g := getg()
gp := g.m.curg
if !block {
unlock(&c.lock)
return false
}
// 创建一个 sudog,表示当前 goroutine 等待发送
sg := acquireSudog()
sg.releasetime = 0
sg.acquiretime = 0
sg.g = gp
sg.elem = ep
sg.c = c
// 将 sudog 加入到发送等待队列中
c.sendq.enqueue(sg)
// 释放锁,并将当前 goroutine 阻塞
unlock(&c.lock)
park_m(gp, waitReasonChanSend, traceEvGoBlockSend, 1)
// 当 goroutine 被唤醒时,重新获取锁
lock(&c.lock)
// 检查 chan 是否已经关闭
if c.closed != 0 {
unlock(&c.lock)
panic("send on closed channel")
}
// 从发送等待队列中取出 sudog
sg = c.sendq.dequeue()
if sg == nil {
throw("chan send inconsistency")
}
// 将数据复制到队列中
qput(c, i, ep)
}
// 更新发送位置
c.sendx++
// 释放锁
unlock(&c.lock)
return true
}
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 获取 chan 的锁
lock(&c.lock)
// 检查 chan 是否已经关闭
if c.closed != 0 && c.qcount == 0 {
unlock(&c.lock)
return false, false
}
// 计算接收位置
i := c.recvx
// 如果队列中没有数据,需要将当前 goroutine 加入到接收等待队列中
if c.qcount <= 0 {
g := getg()
gp := g.m.curg
if !block {
unlock(&c.lock)
return false, false
}
// 创建一个 sudog,表示当前 goroutine 等待接收
sg := acquireSudog()
sg.releasetime = 0
sg.acquiretime = 0
sg.g = gp
sg.elem = ep
sg.c = c
// 将 sudog 加入到接收等待队列中
c.recvq.enqueue(sg)
// 释放锁,并将当前 goroutine 阻塞
unlock(&c.lock)
park_m(gp, waitReasonChanReceive, traceEvGoBlockRecv, 1)
// 当 goroutine 被唤醒时,重新获取锁
lock(&c.lock)
// 检查 chan 是否已经关闭
if c.closed != 0 && c.qcount == 0 {
unlock(&c.lock)
return false, false
}
// 从接收等待队列中取出 sudog
sg = c.recvq.dequeue()
if sg == nil {
throw("chan receive inconsistency")
}
// 从队列中取出数据
qget(c, i, ep)
} else {
// 从队列中取出数据
qget(c, i, ep)
}
// 更新接收位置
c.recvx++
// 更新队列中的元素数量
c.qcount--
// 释放锁
unlock(&c.lock)
return true, true
}