持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
通道简介
go的通道channel是用于协程之间数据通信的一种方式,因为协程goroutine在运行的时候不保证顺序,且是并发(非并行)执行,数据的传递要么通过共享内存(比如指针)传递,要么就是通过一个额外的channel来传递。
其实推荐使用channel来传递数据主要是:
1.避免协程竞争和数据冲突问题,大家都去抢那个共享内存的指针是有冲突的,同时修改又有并发问题,还需要加锁
2.更高级抽象,降低开发难度,管道通信是一种通信方式,只需要监听即可,无需多次轮训来耗费资源
3.模块之间更容易解耦,增加扩展性和可维护性,通道有时候更像是生产者消费者模式,产生数据的协程和接受数据的协程无需知晓对方的存在,只专注自己的事就好
通道长啥样
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
}
- qcount channel 中的元素个数
- dataqsiz channel 中循环缓存队列的长度
- buf channel中缓存区的指针
- elemsize channel收发(元素)的大小
- elemtype channel收发(元素)的类型
- closed 通道关闭的flag,一旦关闭将不能接受新的消息
- sendx sendq,发送队列的指针和发送队列
- recvx recvq ,接收队列的指针和接收队列
- lock 锁,保护整个结构体
通道channel主要是设置了一个环状的缓冲区,其实就是个环形链表,好处就是降低gc的开销,缓冲区的大小是创建channel的时候传入的。当没有设置缓冲区的时候,缓冲区为0,这时候称该通道为无缓冲通道,反之就是有缓冲的通道。
关于两者的区别主要是在阻塞的过程上有差别。
- 对于无缓冲区的channel
- 发送数据方在发送的时候,如果没有接收者,那么发送方的协程groutine阻塞在通道的sendq里面。
- 接收数据方会一直等待数据到来,如果数据一直没有到来,那么接收方的协程groutine就会阻塞在channel的recvq里面。
- 对于有缓冲区的channel
- 在缓冲区没有满的时候,发送方只管发送,当缓冲区存满的时候,发送方的协程groutine就会阻塞,如果这时候有协程过来取数据,那么优先给他缓存里的数据,然后再把sendq里面的协程的数据copy到缓存,并把该协程从阻塞中释放出来,也就是唤醒。
- 当缓冲区没有任何数据,也就是管道里面没有数据的时候,接收方就因为取不到数据而阻塞,进入到recvq里面等待数据到来。当数据到的时候,发送方的协程直接把数据cpoy给接收方的协程,不需额外的经过一次缓冲区。
死锁场景
1. 没有缓冲区的时候,单协程内通道同时写和读
func main() {
// 创建一个通道
ch := make(chan int)
ch <- 1 // 因为接收者在下面,所以阻塞在这里,死锁
num := <-ch
fmt.Println(num)
}
如果不想报死锁,可以加一个缓冲区
ch := make(chan int, 1)
又或者把发送方和接收方各放入一个新的协程
func main() {
ch := make(chan int)
// 发送方
go func() {
ch <- 1
}()
// 接收方
go func() {
num := <-ch
fmt.Println(num)
}()
// 休眠一下,让主协程等子协程处理完,避免提早退出
time.Sleep(1 * time.Second)
}
2.无缓冲区的时候,通道输入数据早于接收方的协程开启
其实和第一种死锁类似,都是没有缓冲的时候,channel的发送方没有接收者,导致阻塞在发送那一行,后面的go协程都无法启动
func main() {
ch := make(chan int)
ch <- 1 //此处阻塞在发送队列sendq里面,导致协程无法继续向下走,开启子协程
go func() {
num := <-ch
fmt.Println(num)
}()
time.Sleep(1 * time.Second)
}
解决方案和第一个死锁的情况一样,设置缓冲区、发送方也放入一个子协程,或者把ch<-1 搬运到go func 代码块后面执行
3.从一个没有数据的channel里拿数据引起的死锁
func main() {
c := make(chan int)
num := <-c //channel里没有数据,直接死锁
fmt.Println(num)
}
可以使用select{}来规避一下这个死锁
func main() {
c := make(chan int)
select {
case num := <-c:
fmt.Println(num)
default:
fmt.Println("没有数据")
}
}
4.循环等待引起的死锁
这个不是go groutine独有的问题,这个就是循环依赖,A协程依赖B协程的数据,B也依赖A的,这种情况很常见。
func main() {
c1 := make(chan int)
c2 := make(chan int)
go func() {
select {
// 当c1有数据的时候,才会给c2发送数据
case num := <-c1:
fmt.Println(num)
c2 <- 2
}
}()
select {
// 当c2有数据的时候,才会给c1发送数据
case num := <-c2:
fmt.Println(num)
c1 <- 1
}
}
这种情况是日常开发,传统语言中也经常看到的死锁,这个就是编程的问题,要修改逻辑才行
5.有缓冲区,收发数据在同一协程,但是缓冲区已满
当缓冲区满了之后,发送数据的协程就被阻塞在当前sendq里面了,此时的情况和第一个死锁的情况差不多,都是收发数据都在同一个协程,而发数据被阻塞后,整个协程就deadlock了
func main() {
c := make(chan int, 2)
for i := 1; i < 5; i++ {
c <- i
}
num := <-c
fmt.Println("num=", num)
}
6.有缓冲区,缓冲区没数据\取光了,继续从channel取数据
这个情况其实和上面第三个死锁一模一样,不过是多了个缓冲区,这种一模一样的情况还要单独拿出来说,是因为在日常开发中很普遍,经常有多个协程都去拿同一个通道的数据,数据都取完了,部分协程没有拿到数据,结果阻塞在那里,变成僵尸协程。
func main() {
c := make(chan int, 2)
for i := 1; i < 3; i++ {
c <- i
}
for i := 0; i < 4; i++ {
go func(i int) {
// 当通道的数据被取完的时候,子协程其实是死锁状态的
// 但是因为主协程main退出了,所以运行的时候没有报deadlock错误,也就是这个死锁在当前例子中是无感知的
num := <-c
fmt.Println(num)
}(i)
}
time.Sleep(2 * time.Second)
}
注意!! 子协程在取完数据后是死锁的,因为永远都不会有新的数据产生了,如果是生产中,很可能这个goroutine一直阻塞在这里不会被回收。所以要用select来处理这种事,避免取不到数据导致的死锁。
func main() {
c := make(chan int, 2)
for i := 1; i < 3; i++ {
c <- i
}
for i := 0; i < 4; i++ {
go func(i int) {
select {
case num := <-c:
fmt.Println(num)
default:
fmt.Println("没有数据了")
}
}(i)
}
time.Sleep(2 * time.Second)
}
运行结果: