本文来源 Golang并发编程有缓冲通道和无缓冲通道(channel) 不同地方在于为了方便理解将代码做了微调
侵删
无缓冲通道
是指在接收前没有能力保存任何值的通道。
这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的,其中任意一个操作都无法离开另一个操作单独存在。
上图所示,如同接力赛。根据图编号观察①两个协程,创建好了通道②一个往通道里放,这时候两边阻塞④这时候另一个协程要接⑤另一个协程取出来,从①-⑤都是阻塞的,⑥才完成交接,才不会阻塞。
再比喻: 就是一个送信人去你家门口送信 ,你不在家 他不走,你一定要接下信,他才会走。
无缓冲channel创建
ch := make(chan int, 0) //第二个参数为0,或者不写第二个参数
如果没有指定缓冲区容量,那么该通道就是同步的,因此会阻塞到发送者准备好发送和接收者准备好接收。
代码案例
package main
import (
"fmt"
"time"
)
func main() {
//创建一个无缓存的channel
ch := make(chan int, 0)
//len(ch)缓冲区剩余数据个数, cap(ch)缓冲区大小,两者这里永远都是0
fmt.Printf("缓冲区剩余数据个数:%d, 缓冲区大小:%d\n", len(ch), cap(ch))
//新建协程
go func() {
for i := 0; i < 3; i++ { //写三次
fmt.Printf("准备写入第【%d】个子协程\n", i)
ch <- i //往chan写内容
//fmt.Printf("len(ch) = %d, cap(ch)= %d\n", len(ch), cap(ch))
fmt.Printf("上面是第【%d】个子协程的写入,我这里会执行到吗\n",i)
}
}()
//延时2秒
fmt.Printf("我要去睡两秒\n")
time.Sleep(3 * time.Second)
fmt.Printf("睡醒了\n")
for i := 0; i < 3; i++ { //必须读三次
fmt.Printf("此时开始读第【%d】个写入\n", i)
num := <-ch //读管道中内容,没有内容前,阻塞
fmt.Printf("第【%d】个写入读到了,值是【%d】\n", i, num)
}
}
执行结果:
缓冲区剩余数据个数:0, 缓冲区大小:0
我要去睡两秒
准备写入第【0】个子协程
睡醒了
此时开始读第【0】个写入
第【0】个写入读到了,值是【0】
此时开始读第【1】个写入
上面是第【0】个子协程的写入,我这里会执行到吗
准备写入第【1】个子协程
上面是第【1】个子协程的写入,我这里会执行到吗
准备写入第【2】个子协程
第【1】个写入读到了,值是【1】
此时开始读第【2】个写入
第【2】个写入读到了,值是【2】
主执行流程:
//延时2秒
fmt.Printf("我要去睡两秒\n")
time.Sleep(3 * time.Second)
fmt.Printf("睡醒了\n")
//两秒钟时间子协程肯定把for循环执行完毕,但这里也会出现阻塞
//阻塞原因是:当执行到往通道写数据是无缓冲的,对方不读之前会阻塞。也就是,在主协程等着子协程写完,但是主协程还没到读的时候,这时候出现阻塞,等到主协程读完数据才会往下走。
可以执行观察一下程序的执行卡顿观察阻塞
for i := 0; i < 3; i++ { //必须读三次
fmt.Printf("此时开始读第【%d】个写入\n", i)
num := <-ch //读管道中内容,没有内容前,阻塞
fmt.Printf("第【%d】个写入读到了,值是【%d】\n", i, num)
}
打印结果分析:首先子协程for循环往管道里写入一个数据,紧接着主协程for循环出现阻塞,然后主协程for循环从管道读数据,读完了打印。主协程打印完,子协程for循环继续给管道数据,但也有可能主协程读完数据没来得及打印,子协程就把数据写入管道并打印完毕,因为两个是同时并行的。
有缓冲通道
指通道可以保存多个值。
如果给定了一个缓冲区容量,那么通道就是异步的,只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行
上图所示:
①右侧的goroutine正在从通道接收一个值。
②右侧的goroutine独立完成了接手值得动作,而左侧的goroutine正在发送一个新值到通道里。
③左侧的goroutine还在向通道发送新值,而右侧的goroutine正在从通道接收另一个值。这个步骤里的两个操作既不是同步,也不会互相阻塞。
④所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存储更多的值
有缓冲channel创建
ch := make(chan int, 3) //容量是3
代码案例
package main
import (
"fmt"
"time"
)
func main() {
//创建一个有缓存的channel
ch := make(chan int, 3) //容量是3
//len(ch)缓冲区剩余数据个数, cap(ch)缓冲区大小
fmt.Printf("缓冲区剩余数据个数:%d, 缓冲区大小:%d\n", len(ch), cap(ch))
//新建协程
go func() {
for i := 1; i <= 10; i++ { //这里数据量大于管道容量,会出阻塞
fmt.Printf("子协程[%d]开始写入: 缓冲区剩余数据个数:%d, 缓冲区大小:%d\n", i, len(ch), cap(ch))
ch <- i //往chan写内容,如果主协程没读的话,写满3个就会阻塞在此
}
}()
//延时
time.Sleep(2 * time.Second)
for i := 1; i <= 10; i++ { //这里数据量大于管道容量,会出阻塞
fmt.Printf("子协程[%d]准备读出: 缓冲区剩余数据个数:%d, 缓冲区大小:%d\n", i, len(ch), cap(ch))
num := <-ch //读管道中内容,没有内容前,阻塞
fmt.Printf("子协程[%d]已读,读出结果为[%d]: 缓冲区剩余数据个数:%d, 缓冲区大小:%d\n", i, num, len(ch), cap(ch))
}
}
结果为
缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[1]开始写入: 缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[2]开始写入: 缓冲区剩余数据个数:1, 缓冲区大小:3
子协程[3]开始写入: 缓冲区剩余数据个数:2, 缓冲区大小:3
子协程[4]开始写入: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[1]准备读出: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[1]已读,读出结果为[1]: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[2]准备读出: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[2]已读,读出结果为[2]: 缓冲区剩余数据个数:2, 缓冲区大小:3
子协程[3]准备读出: 缓冲区剩余数据个数:2, 缓冲区大小:3
子协程[3]已读,读出结果为[3]: 缓冲区剩余数据个数:1, 缓冲区大小:3
子协程[4]准备读出: 缓冲区剩余数据个数:1, 缓冲区大小:3
子协程[4]已读,读出结果为[4]: 缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[5]准备读出: 缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[5]开始写入: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[6]开始写入: 缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[7]开始写入: 缓冲区剩余数据个数:1, 缓冲区大小:3
子协程[8]开始写入: 缓冲区剩余数据个数:2, 缓冲区大小:3
子协程[9]开始写入: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[5]已读,读出结果为[5]: 缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[6]准备读出: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[6]已读,读出结果为[6]: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[7]准备读出: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[7]已读,读出结果为[7]: 缓冲区剩余数据个数:2, 缓冲区大小:3
子协程[8]准备读出: 缓冲区剩余数据个数:2, 缓冲区大小:3
子协程[8]已读,读出结果为[8]: 缓冲区剩余数据个数:1, 缓冲区大小:3
子协程[9]准备读出: 缓冲区剩余数据个数:1, 缓冲区大小:3
子协程[9]已读,读出结果为[9]: 缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[10]准备读出: 缓冲区剩余数据个数:0, 缓冲区大小:3
子协程[10]开始写入: 缓冲区剩余数据个数:3, 缓冲区大小:3
子协程[10]已读,读出结果为[10]: 缓冲区剩余数据个数:0, 缓冲区大小:3
总结一下有缓冲channel和无缓冲channel的特点与不同
无缓冲的与有缓冲channel有着重大差别,那就是一个是同步的 一个是非同步的。
比如
c1:=make(chan int) 无缓冲
c2:=make(chan int,1) 有缓冲
c1<-1
无缓冲: 不仅仅是向 c1 通道放 1,而是一直要等有别的携程 <-c1 接手了这个参数,那么c1<-1才会继续下去,要不然就一直阻塞着。
有缓冲: c2<-1 则不会阻塞,因为缓冲大小是1(其实是缓冲大小为0),只有当放第二个值的时候,第一个还没被人拿走,这时候才会阻塞。