通道类型篇(一):基本语法和缓冲通道
在上篇教程中,学院君给大家演示了如何通过通道(channel)传递消息实现 Go 协程间的通信, 接下来,我们将通过几篇教程的篇幅来系统了解通道类型及其使用,从而更好地理解 Go 并发编程及其实现,我们首先从通道基本语法说起。
通道声明和初始化
通过上篇教程,想必你已经了解了通道类型的基本使用,我们可以通过 chan
类型关键字来声明通道类型变量:
var ch chan int
上面这个表达式表示声明一个通道类型变量 ch
,并且通道中只能传递 int
类型数据。
与其他数据类型不同,通道类型变量除了声明通道类型本身外,还要声明通道中传递数据的类型,比如这里我们指定这个数据类型为 int
。前面学学院君介绍过,通道是类型相关的,我们必须在声明通道的时候同时指定通道中传递数据的类型,并且一个通道只能传递一种类型的数据,这一点和数组/切片类似。
此外,我们还可以通过如下方式声明通道数组、切片、字典,以下声明方式表示 chs
中的元素都是 chan int
类型的通道:
var chs [10]chan int
var chs []chan int
var chs map[string]chan int
不过,实际编码时,我们更多使用的是下面这种快捷方式同时声明和初始化通道类型:
ch := make(chan int)
由于在 Go 语言中,通道也是引用类型(和切片、字典一样),所以可以通过 make
函数进行初始化,在通过 make
函数初始化通道时,还可以传递第二个参数,表示通道的容量:
ch := make(chan int, 10)
第二个参数是可选的,用于指定通道最多可以缓存多少个元素,默认值是 0,此时通道可以被称作非缓冲通道,表示往通道中发送一个元素后,只有该元素被接收后才能存入下一个元素,与之相对的,当缓存值大于 0 时,通道可以称作缓冲通道,即使通道元素没有被接收,也可以继续往里面发送元素,直到超过缓冲值,显然设置这个缓冲值可以提高通道的操作效率。
需要注意的是,我们在上一篇教程中通过下面这种方式初始化的是切片,而不是通道:
chs := make([]chan int, 10)
只是切片中的元素类型是通道,这个时候第二个参数是切片的初始容量,而不是通道的。
通道操作符
通道类型变量只支持发送和接收操作,即往通道中写入数据和从通道中读取数据,对应的操作符都是 <-
,我们判断是发送还是接收操作的依据是通道类型变量位于 <-
左侧还是右侧,位于左侧是发送操作,位于右侧是接收操作:
ch <- 1 // 往通道中写入数据 1
x := <- ch // 从通道中读取数据并赋值给变量
当我们将数据发送到通道时,发送的是数据的副本,同理,从通道中接收数据时,接收的也是数据的副本。
上篇教程我们已经介绍过,发送和接收操作都是原子操作,同时只能进行发送或接收操作,不存在数据发送一半被接收,或者接收一半发送新数据的情况,并且两者都是阻塞的,如果通道中没有数据,进行读取操作的话会导致读取操作所在的协程阻塞,直到通道中写入了数据;反过来,如果通道中已经有了数据,再往里面写入数据的话,也会导致写入操作所在的协程阻塞,直到其中的数据被其他协程接收。
使用缓冲通道提升性能
当然,上面这种情况发生在非缓冲通道中,对于缓冲通道,情况略有不同,假设 ch
是通过 make(chan int, 10)
进行初始化的通道,则其缓冲区大小是 10,这意味着,在没有被任何其他协程接收的情况下,我们可以一直往 ch
通道中写入 10 个数据,超过 10 个数据才会阻塞当前协程,直到通道被其他协程读取,显然,合理设置缓冲区可以提高通道的操作效率,尤其是在需要持续传输大量数据的场景。
我们可以通过如下示例代码简单测试下通道的缓冲机制:
package main
import (
"fmt"
"time"
)
func test(ch chan int) {
for i := 0; i < 100; i++ {
ch <- i
}
close(ch)
}
func main() {
start := time.Now()
ch := make(chan int, 20)
go test(ch)
for i := range ch {
fmt.Println("接收到的数据:", i)
}
end := time.Now()
consume := end.Sub(start).Seconds()
fmt.Println("程序执行耗时(s):", consume)
}
我们在主协程中初始化了一个带缓冲的通道,缓冲大小是 20,然后将其传递到子协程,并且在子协程中发送数据到通道,子协程执行完毕后,调用 close(ch)
显式关闭通道,这一行不能漏掉,否则主协程不知道子协程什么时候执行完毕,从一个空的通道接收数据会报如下运行时错误(死锁):
fatal error: all goroutines are asleep - deadlock!
关闭通道的操作只能执行一次,试图关闭已关闭的通道会引发 panic。此外,关闭通道的操作只能在发送数据的一方关闭,如果在接收一方关闭,会导致 panic,因为接收方不知道发送方什么时候执行完毕,向一个已经关闭的通道发送数据会导致 panic。
回到主协程,我们通过 i := range ch
循环从通道中读取数据,并将其打印出来。当通道关闭后会退出循环。我们对主协程执行时间做了统计,以对比不使用缓冲通道的耗时。
使用缓冲通道,程序执行耗时打印结果如下:
程序执行耗时(s): 0.000779079
然后我们将 ch
通道初始化语句调整为 ch := make(chan int)
,再次执行,程序耗时如下:
程序执行耗时(s): 0.001526328
显然,使用缓冲通道程序性能更好。
7 Comments
需要 加黑一段字
对于同一通道,发送操作在接受者准备好之前是不会结束的。这就意味着,如果一个无缓冲通道在没有空间接收数据的时候,新的输入数据无法输入,即发送者处于阻塞状态。
发现自己踩坑了,当时不明其理
是的,无缓冲通道必须有两个协程同时运行,一个写入,一个读出。如何在单核情况下,是不是程序会运行不了啊
这边应该要注意到go语言的中,通道关闭后是可以继续读数据的。
"并且两者都是是阻塞的"-->“并且两者都是阻塞的”
纠错小能手 ?
接收和发送并非是不可同时发生的,对于无缓冲通道,发送操作是在接收操作未完成前完成的
学习打卡