目录概念:使用场景:底层数据结构:操作:创建发送接收关闭案例分析:概念: Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来
Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来通信,而要通过通信来实现内存共享,CSP(Communicating Sequential Process)并发模型,就是通过 goroutine 和 channel 来实现的)
停止信号监听
定时任务
生产方和消费方解耦
控制并发数
通过var声明或者make函数创建的channel变量是一个存储在函数栈帧上的指针,占用8个字节,指向堆上的hchan结构体
源码包中src/runtime/chan.go
定义了hchan的数据结构:
hchan结构体:
type hchan struct {
closed uint32 // channel是否关闭的标志
elemtype *_type // channel中的元素类型
// channel分为无缓冲和有缓冲两种。
// 对于有缓冲的channel存储数据,使用了 ring buffer(环形缓冲区) 来缓存写入的数据,本质是循环数组
// 为啥是循环数组?普通数组不行吗,普通数组容量固定更适合指定的空间,弹出元素时,普通数组需要全部都前移
// 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
buf unsafe.Pointer // 指向底层循环数组的指针(环形缓冲区)
qcount uint // 循环数组中的元素数量
dataqsiz uint // 循环数组的长度
elemsize uint16 // 元素的大小
sendx uint // 下一次写下标的位置
recvx uint // 下一次读下标的位置
// 尝试读取channel或向channel写入数据而被阻塞的goroutine
recvq waitq // 读等待队列
sendq waitq // 写等待队列
lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}
等待队列:
双向链表,包含一个头结点和一个尾结点
每个节点是一个sudog结构体变量,记录哪个协程在等待,等待的是哪个channel,等待发送/接收的数据在哪里
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer
c *hchan
...
}
使用 make(chan T, cap)
来创建 channel,make 语法会在编译时,转换为 makechan64
和 makechan
func makechan64(t *chantype, size int64) *hchan {
if int64(int(size)) != size {
panic(plainError("makechan: size out of range"))
}
return makechan(t, int(size))
}
创建channel 有两种,一种是带缓冲的channel,一种是不带缓冲的channel
// 带缓冲
ch := make(chan int, 3)
// 不带缓冲
ch := make(chan int)
创建时会做一些检查:
创建时的策略:
发送操作,编译时转换为runtime.chansend
函数
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
阻塞式:
调用chansend函数,并且block=true
ch <- 10
非阻塞式:
调用chansend函数,并且block=false
select {
case ch <- 10:
...
default
}
向 channel 中发送数据时大概分为两大块:检查和数据发送,数据发送流程如下:
如果 channel 的读等待队列存在接收者goroutine
如果 channel 的读等待队列不存在接收者goroutine
发送操作,编译时转换为runtime.chanrecv
函数
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
阻塞式:
调用chanrecv函数,并且block=true
<ch
v := <ch
v, ok := <ch
// 当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值
for i := range ch {
fmt.Println(i)
}
非阻塞式:
调用chanrecv函数,并且block=false
select {
case <-ch:
...
default
}
向 channel 中接收数据时大概分为两大块,检查和数据发送,而数据接收流程如下:
如果 channel 的写等待队列存在发送者goroutine
如果 channel 的写等待队列不存在发送者goroutine
关闭操作,调用close函数,编译时转换为runtime.closechan
函数
close(ch)
func closechan(c *hchan)
package main
import (
"fmt"
"time"
"unsafe"
)
func main() {
// ch是长度为4的带缓冲的channel
// 初始hchan结构体重的buf为空,sendx和recvx均为0
ch := make(chan string, 4)
fmt.Println(ch, unsafe.Sizeof(ch))
go sendTask(ch)
go receiveTask(ch)
time.Sleep(1 * time.Second)
}
// G1是发送者
// 当G1向ch里发送数据时,首先会对buf加锁,然后将task存储的数据copy到buf中,然后sendx++,然后释放对buf的锁
func sendTask(ch chan string) {
taskList := []string{"this", "is", "a", "demo"}
for _, task := range taskList {
ch <- task //发送任务到channel
}
}
// G2是接收者
// 当G2消费ch的时候,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁
func receiveTask(ch chan string) {
for {
task := <-ch //接收任务
fmt.Println("received", task) //处理任务
}
}
总结hchan结构体的主要组成部分有四个:
以上就是Go底层channel实现原理及示例详解的详细内容,更多关于Go channel底层原理的资料请关注编程网其它相关文章!
--结束END--
本文标题: Go底层channel实现原理及示例详解
本文链接: https://www.lsjlt.com/news/120981.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
下载Word文档到电脑,方便收藏和打印~
2024-04-05
2024-04-05
2024-04-05
2024-04-05
2024-04-05
2024-04-05
2024-04-05
2024-04-05
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0