Channel的理念
Channel非常重要,在Golang中有个重要思想:不以共享内存通信,而以通信来共享内存
Javaer应该能理解这句话是什么意思,在Java中我们无法在多线程之间通过直接传递的方式来共享数据
而是以一个比如静态变量这么个东西在一个类中,多个线程去修改这个静态变量的方式来进行通信共享数据
Channel是什么
Channel是一个可以收发数据的管道,简单来说就是可以在协程间传输数据的一个管道
channel的声明方式:
var channel_name chan channel_type
var channel_name [size]chan channel_tyepe
声明之后的管道,并没有进行初始化为其分配空间,其值是nil,我们要使用的话需要配合make函数进行初始化
var channel_name chan channel_type
channel_name = make(chan channel_type)
channel_name = make(chan channel_type, size)
也可以一步完成
channel_name := make(chan channel_type)
channel_name := make(chan channel_type, size) // 创建带有缓存的管道 size为缓存大小 下文会讲缓冲和非缓冲管道
Channel能做什么
Channel是一个可以收发数据的管道,简单来说就是可以在协程间传输数据的一个管道
ch := make(chan int)
go func() {
ch <- 1 // 发送操作会阻塞,直到有接收者
fmt.Println("发送成功")
}()
num := <-ch // 接收操作会阻塞,直到有发送者
- 怎么读取Channel的数据
func main() {
ch := make(chan int, 5)
ch <- 1
ch <- 2
close(ch)
go func() {
for value := range ch {
fmt.Printf("value = %d\n",value)
}
}()
time.Sleep(2 * time.Second)
}
Channel - 有缓冲和无缓冲
-
无缓冲Channel (同步Channel)
发送和接收操作必须同时进行,当一个协程尝试向无缓冲Channel发送数据时
若没有其他协程准备好接收,发送操作会阻塞当前协程,直到有接收者,接收操作也是如此
ch := make(chan int) go func() { ch <- 1 // 发送操作会阻塞,直到有接收者 fmt.Println("发送成功") }() num := <-ch // 接收操作会阻塞,直到有发送者
-
有缓冲Channel (异步Channel)
允许发送和接收操作在一定程度上异步进行,当缓冲区未满的时候,发送操作不会阻塞
当缓冲区满的时候,发送操作会阻塞,直到有接收者从Channel取出数据
当Channel未关闭的时候,缓冲区为空的时候,接收操作会阻塞,直到有数据被发送到Channel
当Channel关闭了的时候,接收操作会立即返回,并得到该Channel元素类型的零值
ch := make(chan int, 3) // 创建一个容量为3的有缓冲Channel ch <- 1 // 不会阻塞 ch <- 2 // 不会阻塞 ch <- 3 // 不会阻塞 // ch <- 4 // 此时会阻塞,因为缓冲区已满 直到有接收操作取出缓冲区的数据
Channel - 单向和双向 (不重要)
Channel根据其功能又可以分为双向Channel和单向Channel,双向Channel既可以发送数据又可以接收数据
单向Channel要么只能发送数据,要么只能接收数据,注意双向Channel不是意味着Channel的两端能出能进
-
定义单向读Channel
var ch = make(chan int) type RChannel= <-chan int // 定义类型 var rec RChannel = ch
-
定义单向写Channel
var ch = make(chan int) type SChannel = chan<- int // 定义类型 var send SChannel = ch
-
代码示例
import ( "fmt" "time" ) type SChannel = chan<- int type RChannel = <-chan int func main() { var ch = make(chan int) // 创建channel go func() { var send SChannel = ch fmt.Println("send: 100") send <- 100 }() go func() { var rec RChannel = ch num := <- rec fmt.Printf("receive: %d", num) }() time.Sleep(2*time.Second) }
一般我们都不会进行单向Channel的使用,知道就行
Channel的底层实现原理 (定义及数据结构)
Channel的底层定义,Channel用make函数创建初始化的时候会在堆上分配一个runtime.hchan类型的数据结构
type hchan struct {
qcount uint // 循环队列的元素总数
dataqsiz uint // 循环队列的大小
buf unsafe.Pointer // 指向循环队列的指针
elemsize uint16 // 循环队列中的每个元素的大小
closed uint32 // 标记位 标记Channel是否关闭
elemtype *_type // 循环队列中的元素类型
sendx uint // 已发送元素在循环队列中的索引位置
recvx uint // 已接收元素在循环队列中的索引位置
recvq waitq // 等待向Channel接收消息的sudog队列
sendq waitq // 等待向Channel写入消息的sudog队列
lock mutex // 互斥锁,对Channel的数据读写操作进行加锁,保证并发安全
}
可以看到Channel的底层实现是有锁的,是通过Mutex来保证线程安全的,也就是说Channel是一个线程安全的组件
Channel的底层数据结构
Channel的读写操作
- 写操作
- 加锁保护,lock(&c.lock) 对hchan结构体加互斥锁,确保操作原子性(避免并发修改缓冲区或等待队列)
- 查看接收队列recvq是否存在sudog在等待数据,如果有的话,直接将数据发给队首的接收协程 (跳过缓冲区),将数据从发送协程复制到接收协程的存储地址,唤醒接收协程,解锁并返回
- 处理有缓冲Channel:
- 若缓冲区未满 (c.qcount < c.dataqsiz):将数据放入缓冲区中sendx指向的位置,并对sendx进行移位 (sendx + 1) % dataqsiz,同时更新c.qucount(元素数量 + 1),解锁并返回
- 若缓冲区已满 (c.qcount == c.dataqsiz):将当前发送协程加入发送队列 (sendq),标记位阻塞状态,解锁并返回
- 处理无缓冲Channel:
- 无缓冲区,数据必须直接发送给接收协程,将当前发送协程加入发送队列 (sendq),阻塞等待接收者出现,解锁并返回
- 读操作
- 加锁保护,lock(&c.lock) 对hchan结构体加互斥锁,确保操作原子性(避免并发修改缓冲区或等待队列)
- 查看发送队列sendq是否存在sudog在发送数据,如果有的话,直接从发送协程获取数据 (跳过缓冲区),将数据从发送协程复制到接收协程的存储地址,唤醒发送协程,发送协程的阻塞状态解除,可继续执行,解锁并返回
- 处理有缓冲Channel:
- 若缓冲区非空 (c.qcount > 0):从缓冲区中recvx只想的位置读取数据,更新c.qcount (元素数量 -1),recvx++环形递增,解锁并返回
- 若缓冲区为空 (c.qcount = 0):将当前接收协程加入接收队列recvq,标记位阻塞状态,解锁并返回
- 处理无缓冲Channel:
- 无缓冲区,必须等待发送协程发送数据,若发送队列为空,则进入接收队列(这一步检查是没有的,因为第二步已经检查过了)
关于Channel的总结
- 关闭一个未初始化的Channel会产生Panic
- Channel只能被关闭一次,对同一个Channel重复关闭会产生Panic
- 向一个已经关闭了的Channel发送消息会产生Panic
- 从一个已经关闭的Channel读取消息不会发生Panic,会一直读取数据(零值)
- Channel是并发安全的,多个Goroutine同时读取Channel中的数据,不会产生并发安全问题