文章目录
前言
最近在学习Golang的时候,注意到channel
的传输存在很多种情况,因此在本篇文章中通过实践的方式,整理各种情况的结果,方便自己和大家今后的学习。
channel可以分为3种类型:
//只读 channel,单向 channel
readOlnyChan := make(<-chan int)
只写 channel,单向 channel
writeOlnyChan := make(chan<- int)
可读可写 channel
ch := make(chan)
三个类型种,业务最频繁使用的是可读可写的无限制channel,所以接下来将主要介绍可读可写的channel场景。
各种情况预览
总结下来,异常主要有三个部分。
- 超量读写 和 nil读写: 出现了阻塞
- close的channel写入: 触发
panic
- close的读:读出0
Channel的两种使用
Golang中channel
的存在使得整个语言在处理异步式或密集IO的问题有着显著的提升,因为Golang的设计,近来的JAVA在jdk21版本设计上也参照Golang的协程理念。
Channel在英文中即管道的意思,在Golang中channel
就像管道一样,承担着在多个程序之间传输的任务。针对不同情况,可以创建两种Channel:无缓冲Channel、有缓冲Channel。
无缓冲Channel
创建一种int类型的无缓冲channel,其结果代表协程中处理的最终结果。最终被主线程接受
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
// 执行任务
fmt.Println("执行协程任务开始")
time.Sleep(1 * time.Second)
fmt.Println("执行协程任务结束")
// 假设得到结果 为1
// 传1到channel中
ch <- 1
}()
// 主线程异步执行其他任务
fmt.Println("执行主线程任务开始")
time.Sleep(2 * time.Second) //假设执行需要2s
fmt.Println("执行主线程任务结束")
select {
case x := <-ch:
fmt.Println("接受Channel", x)
case <-time.After(time.Second):
fmt.Println("timeout")
}
}
可以看到主线程任务和协程任务,两者同时开始,并且在最后将channel中的值传给了主线程。这两者在执行任务的过程是同步。当然还有更多中关于无缓冲Channel的使用,这里就不多介绍。
有缓冲Channel的使用
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
// 1. 正常使用buffer
// 结果制造程序
go func() {
// 程序需执行三次计算,每次计算耗时1s
for i := 0; i < 3; i++ {
time.Sleep(1 * time.Second)
fmt.Println("任务结束,传输的值为:", i)
ch <- i
fmt.Println("传输结果:", i)
}
}()
// 结果接受程序
for i := 0; i < 3; i++ {
select {
case v := <-ch:
fmt.Println("获取结果:", v)
case <-time.After(3 * time.Second):
fmt.Println("正常读写出现阻塞")
}
}
}
通过make(chan int 3)
来创建一个int
类型的有缓冲buffer,其中3表示缓冲的长度。上述代码运行结果如下。
上述就简单的演示一下有缓冲channel的一种使用情况。
无缓冲Channel的所有情况
无缓冲Channel在使用中主要可能出现5种读写场景。
正常读写
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
// 1. 正常channel读写
fmt.Println("----------正常channel读写-----------------")
go func() {
ch <- "1. 正常读写"
}()
select {
case data := <-ch:
fmt.Println(data, "读写成功")
case <-time.After(time.Second):
fmt.Println("1. 正常读写:1s后读写阻塞了。。。")
}
}
标准的无缓冲channel读写
连续写入
// 2.连续写
ch = make(chan string)
flag := make(chan bool)
fmt.Println("----------连续写-----------------")
go func() {
for i := 0; i < 3; i++ {
fmt.Printf("第%d次写入\n", i+1)
ch <- "这是第" + string(rune(i)) + "次"
}
flag <- true
// 循环是否出来
fmt.Println("循环结束")
}()
select {
case <-flag:
fmt.Println("没有阻塞")
case <-time.After(time.Second):
fmt.Println("2.连续写:阻塞了")
}
如果无缓冲channel中不是空的,那么下次写入就会阻塞,直到有程序将其读出
package main
import (
"fmt"
"time"
)
func main() {
// 3. 连续读
fmt.Println("----------连续读-----------------")
// 创建一个空channel
ch := make(chan string)
flag := make(chan bool)
go func() {
// 持续读
for i := 0; i < 3; i++ {
fmt.Printf("第%d次读\n", i+1)
x := <-ch
fmt.Println("ch的值为:", x)
}
flag <- true
}()
select {
case <-flag:
fmt.Println("没有阻塞")
case <-time.After(time.Second):
fmt.Println("3. 连续读:阻塞了")
}
}
类似的,如果无缓冲buffer中没有数据,那么读也会被阻塞,直到channel中出现写入。
nil的channel
写入情况
package main
import (
"fmt"
"time"
)
func main() {
var ch chan string = nil
// 4. nil的channel读写
fmt.Println("----------nil的channel读写-----------------")
go func() {
ch <- "4. nil的channel读写"
fmt.Println("写入完成")
}()
select {
case data := <-ch:
fmt.Println(data, "读写成功")
case <-time.After(time.Second):
fmt.Println("4. 正常读写:1s后读写阻塞了。。。")
}
}
可以看到,协程中的写入操作 = 没有写入。并且fmt.Println("写入完成")
部分也没有执行,说明写入部分出现了阻塞,主线程在通过select
的操作时并没有检查到ch中的输入信号,所以最终倒计时结束执行退出操作。
读取状况
package main
import (
"fmt"
"time"
)
func main() {
var ch chan int = nil
go func() {
fmt.Println(<-ch)
fmt.Println("读取成功")
}()
time.Sleep(1 * time.Second)
}
运行结果也并没出现读取成功的字样,所以协程中也出现了阻塞的情况。
已关闭的channel
写入情况
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
close(ch)
// 5. channel关闭情况下读写
fmt.Println("----------channel关闭情况下读写-----------------")
go func() {
ch <- "5. close的channel读写"
}()
time.Sleep(1 * time.Second)
}
可以看到在向关闭的channel写入时,会触发panic
,从而终止程序。
读取情况
import "fmt"
func main() {
ch := make(chan int)
close(ch)
fmt.Println("读取channel")
x := <-ch
fmt.Println(x)
}
代码结果表明,在读取close
的channel
时,会返回一个0
作为结果,而不会像无值的时候那样出现阻塞。
总结
将上述的情况进行总结,可以得到有缓冲channel在多种情况下,在每个程序中会带来的影响。
主要有两个特殊情况,即close读写的两个特殊情况。
- close的channel写会造成
panic
- close的channel读在int类型下会读到0。
有缓冲Channel的所有情况
Channel中还有有缓冲的设计模式,通过在创建时,对channel设定一定的大小的长度,来表示channel最大能缓冲的数据量。
正常读写
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
fmt.Println("-----------正常读写---------------")
// 1. 正常使用buffer
// 结果制造程序
go func() {
// 程序需执行三次计算,每次计算耗时1s
for i := 0; i < 3; i++ {
time.Sleep(1 * time.Second)
fmt.Println("任务结束,传输的值为:", i)
ch <- i
fmt.Println("传输结果:", i)
}
}()
// 结果接受程序
for i := 0; i < 3; i++ {
select {
case v := <-ch:
fmt.Println("获取结果:", v)
case <-time.After(3 * time.Second):
fmt.Println("正常读写出现阻塞")
}
}
}
代码中一个协程进行数据的筛入,主线程进行获取。最终很好的实现三次传输和接受的同步。并且整个程序也没有出现阻塞的现象。
超量写不读
package main
import (
"fmt"
"time"
)
func main() {
// 2. 超量写
fmt.Println("----------超量写-----------")
ch := make(chan int, 3)
flag := make(chan bool)
go func() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("程序结束,传输数据", i)
ch <- i
fmt.Println("传输成功,", i)
}
fmt.Println("数据传输完毕")
// 没有阻塞
flag <- true
}()
select {
case <-flag:
fmt.Println("没有阻塞")
case <-time.After(6 * time.Second):
fmt.Println("6s 后发现阻塞")
}
}
可以看到只写不读的情况下,当传入channel循环到第三个数据时,就不会传输了,此协程中就出现了阻塞。最终在主线程中结束了等待。
超量读不写
package main
import (
"fmt"
"time"
)
func main() {
// 3. 超量读
fmt.Println("----------超量读-----------")
// 重新创建新的
ch := make(chan int, 3)
flag := make(chan bool)
go func() {
for i := 0; i < 5; i++ {
fmt.Println("程序计算完毕,准备获取channel内部的值")
v := <-ch
fmt.Println("此时无值,但要超量读,v的值为:", v)
}
fmt.Println("channel数据获取完毕")
flag <- true
}()
select {
case <-flag:
fmt.Println("没有阻塞")
case <-time.After(6 * time.Second):
fmt.Println("6s 后发现阻塞")
}
}
可以看到,在协程中channel根本就没有数据,所以自然而然就发生了阻塞,进而协程中的程序将不再执行,直到主线程等待6s后结束程序。
nil 的有缓冲channel的情况
写
package main
import (
"fmt"
"time"
)
func main() {
var ch chan []int = nil
flag := make(chan bool)
go func() {
ch <- []int{1, 2, 3}
flag <- true
fmt.Println("数据写入成功")
}()
select {
case <-flag:
fmt.Println("channel数据写入成功")
case <-time.After(time.Second):
fmt.Println("1s后出现阻塞")
}
}
可以看到在新建的协程中,执行ch <- []int{1, 2, 3}
时出现了阻塞,协程无法继续向后执行,flag无法接受到结束信号,使的整个协程出现了阻塞。直到主线程中select
将整个进程按阻塞情况处理了。
读
package main
import (
"fmt"
"time"
)
func main() {
var ch chan []int = nil
flag := make(chan bool)
go func() {
x := <-ch
fmt.Println("x:", x)
flag <- true
fmt.Println("数据写入成功")
}()
select {
case <-flag:
fmt.Println("程序结束")
case <-time.After(1 * time.Second):
fmt.Println("程序阻塞")
}
}
结果也是出现了阻塞,当然这也是符合预期的。
关闭缓冲Channel的所有情况
正常读写
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 3)
close(ch)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
fmt.Println("数据传输成功")
}()
for i := 0; i < 3; i++ {
select {
case v := <-ch:
fmt.Println("收到数据,", v)
case <-time.After(time.Second):
fmt.Println("阻塞")
}
}
}
当执行上述的关闭程序时,发现出现了收到了数据和send on closed channel
的错误,猜测写入关闭channel
会出现panic
,读的时候会读到0。也就是说有缓冲和无缓冲的channel
的关闭情况是相同的。
总结
结果可以看到,有缓冲的channel的各种情况的和无缓冲的channel相类似。
最后总结
可以看到,不管是无缓冲和有缓冲channel
,两者的机理相类似,这必然是golang中内部源码提前设计好的,如果大家喜欢的话,之后可以再深入探讨一下^-^
总结下来,异常主要有三个部分。
- 超量读写 和 nil读写: 出现了阻塞
- close的channel写入: 触发
panic
- close的读:读出0
本文是经过个人查阅相关资料后理解的提炼,可能存在理论上理解偏差的问题,如果您在阅读过程中发现任何问题或有任何疑问,请不吝指出,我将非常感激并乐意与您讨论。谢谢您的阅读!