大家好,今天,我们要讨论的主题是 Go 语言中管道(Channel)的实现原理。Go 语言的并发模型非常独特,管道在其中扮演着重要的角色。那么,究竟什么是管道?它是如何工作的呢?
一、什么是管道?
在 Go 语言中,管道是一种特殊的类型,可以用来在不同的 Goroutine 之间传递数据。你可以把它想象成一条传送带或者一个管道,数据可以从一头进入,然后从另一头出来。 注意:每一个管道只能存储一种类型的数据。
var ch chan int // 声明管道
ch := make(chan int) // 创建一个整数类型的管道
二、管道是如何工作的?
管道的数据传输总是遵循先进先出(FIFO)的原则,也就是说,在多个 Goroutine 向同一个管道发送数据的时候,数据的出现顺序会与发送顺序一致。同样的,接收操作也是遵循这个原则。
ch <- 10 // 发送数据到管道
x := <-ch // 从管道接收数据
操作符 “<-” 表示数据流向:管道在左表示向管道写入数据,管道在右表示从管道读出数据。
当你执行 x := <-ch 时,Go 会从 hchan 的缓冲区中复制一个值到 x。如果缓冲区为空,那么接收操作会被阻塞,直到有其他 Goroutine 向管道发送了数据。
当你执行 ch <- x 时,Go 会将 x 的值复制到 hchan 的缓冲区中。如果缓冲区已满,那么发送操作会被阻塞,直到有其他 Goroutine 从管道中接收了数据。
默认的管道为双向可读写,管道在函数间传递时,可以使用操作符进行限制管道的读写,比如:
// 管道 ch 可读写
func chanRW(ch chan int) {
}
// 管道 ch 只可读
func chanR(ch <-chan int) {
}
// 管道 ch 只可写
func chanR(ch chan<- int) {
}
三、管道的实现原理
Go 语言的管道实现涉及到了很多复杂的数据结构和算法,我们这里只介绍最核心的部分。
数据结构
在 Go 的源代码中,管道是通过一个名为 hchan 的结构体实现的,它的定义包括了数据队列、等待队列等信息,它看起来是这样的:
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 时指定的,即 dataqsiz
等待队列
从 channel 读取数据,如果 channel 缓存区为空或者没有缓存区,当前 goroutine 会被阻塞,向 channel 写数据,如果 channel 缓存区已满或者没有缓存区,则当前 goroutine 会被阻塞。
被阻塞的 goroutine 将会被挂在 channel 的等待队列中:
- 因读阻塞的 goroutine 会被向 channel 写入数据的 goroutine 唤醒。
- 因写阻塞的 goroutine 会被向 channel 读数据的 goroutine 唤醒。
向 channel 写数据
向一个 channel 中写数据简单过程:
- 如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出G,并把数据写入,最后把该 G 唤醒,结束发送过程。
- 如果缓冲区有空余位置,将数据写入缓冲区,结束发送过程。
- 如果缓冲区没有空余位置,将待发送数据写入 G ,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒。
向 channel 读数据
向一个 channel 中读数据简单过程:
- 如果等待发送队列 sendq 不为空且没有缓冲区,直接从 sendq 中取出 G ,把 G 数据读出,最后把 G 唤醒,结束读取过程。
- 如果等待发送队列 sendq 不为空,此时说明缓冲区已满,说明此时缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。
- 如果缓冲区有数据,则从缓冲区取出数据,结束读取过程。
- 将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。
四、总结
管道是 Go 语言并发模型的核心组成部分,理解其实现原理对于深入理解 Go 语言是非常有帮助的。管道的实现涉及到了许多计算机科学中的基本概念,例如队列、阻塞、唤醒等,通过学习管道,你也可以增进对这些概念的理解。
希望这篇文章能帮助你理解 Go 语言的管道实现原理。如果你有任何问题或者建议,欢迎在下面留言。在下次的文章中,我们将继续探讨 Go 语言的其他知识点。
请关注公众号【Java千练】,更多干货文章等你来看!