一、概述
在Go语言中,channel是一种特殊的类型,用于在并发编程中实现不同的goroutine之间的通信和同步。本文将深入探讨golang的channel是如何工作的,并介绍如何使用channel来提高程序的性能和可靠性。
二、什么是Channel?
在Go语言中,使用goroutine单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。
Channel提供了一种同步的机制,确保在数据发送和接收之间的正确顺序和时机。通过使用channel,我们可以避免在多个goroutine之间共享数据时出现的竞争条件和其他并发问题。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
Channel的操作符是箭头 <- (箭头的指向就是数据的流向)。
三、Channel 类型
Channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
示例:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔值的通道
var ch3 chan []int // 声明一个传递int切片的通道
var ch4 chan struct{} // 声明一个struct的通道
Channel类型的定义格式如下:
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
它包括三种类型的定义。可选的<-
代表channel的方向。如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。
chan T // 可以接收和发送类型为 T 的数据
chan<- float64 // 只可以用来发送 float64 类型的数据
<-chan int // 只可以用来接收 int 类型的数据
<-
总是优先和最左边的类型结合。
chan<- chan int // 等价 chan<- (chan int)
chan<- <-chan int // 等价 chan<- (<-chan int)
<-chan <-chan int // 等价 <-chan (<-chan int)
chan (<-chan int)
四、创建 Channel
通道是引用类型,通道类型的空值是 nil
var ch chan int
fmt.Println(ch) // 结果是: <nil>
声明后的通道需要使用 make 函数初始化之后才能使用。
使用make初始化channel格式:
make(chan 元素类型, [容量])
容量(capacity)代表Channel容纳的最多的元素的数量,代表Channel的缓存的大小。
如果没有设置容量,或者容量设置为0, 说明Channel没有缓存,只有sender和receiver都准备好了后它们的通讯(communication)才会发生(Blocking)。如果设置了缓存,就有可能不发生阻塞, 只有buffer满了后 send才会阻塞, 而只有缓存空了后receive才会阻塞。一个nil channel不会通信。
所以Channel可分为:无缓冲通道(阻塞),有缓存通道(非阻塞)
五、Channel 操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。
现在我们先使用以下语句定义一个通道:
ch := make(chan int)
发送(send)
send被执行前(proceed)通讯(communication)一直被阻塞着。如前所言,无缓存的channel只有在receiver准备好后send才被执行。如果有缓存,并且缓存未满,则send会被执行。
往一个已经被close的channel中继续发送数据会导致run-time panic。
往nil channel中发送数据会一致被阻塞着。
将一个值发送到通道中。
ch <- 10 // 把 10 发送到 ch 中
接收(receive)
从一个nil channel中接收数据会一直被block。
从一个被close的channel中接收数据不会被阻塞,而是立即返回,接收完已发送的数据后会返回元素类型的零值(zero value)。
从一个通道中接收值
x := <-ch // 从 ch 中接收值并赋值给变量x
<-ch // 从 ch 中接收值,忽略结果
关闭(close)
可以通过内建的close
方法可以关闭Channel。关闭channel后,任何接收方将收到一个零值和一个布尔标志,指示channel已关闭。
close(ch)
如果你的管道不往里存值或者取值的时候一定记得关闭管道。
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致panic。
v, ok := <-ch
它可以用来检查Channel是否已经被关闭了。
如果OK 是false,表明接收的x是产生的零值,这个channel被关闭了或者为空。
六、无缓冲的通道(阻塞)
无缓冲的通道又被称为阻塞的通道。 我们看一下示例:
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行时会出现一下错误:
fatal error: all goroutines are asleep - deadlock!
为什么会出现deadlock错误呢?
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?
一种方法是启用一个goroutine去接收值,例如:
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
七、有缓冲的通道(非阻塞)
解决无缓冲通道(阻塞)死锁的问题,就是使用有缓冲的通道。
通过缓存的使用,可以尽量避免阻塞,提供应用的性能。
我们使用 make 函数在初始化的时候为其指定通道的容量(缓冲大小):
func main(){
ch := make(chan int ,1) // 创建一个容量为 1 的有缓冲区的通道
ch <- 10
fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
八、单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况:
// 单向发送 out 通道
func cter(out chan <- int){
for i := 0; i < 10; i++ {
out <- i
}
close(out)
}
// 单向发送 out 通道, 单向接收 in 通道
func sqer(out chan <- int , in <- chan int){
for i := range in{
out <- i * i
}
close(out)
}
// 单向接收 in 通道
func prter(in <-chan int){
for i := range in {
fmt.Println(i)
}
}
func main(){
out := make(chan int)
in := make(chan int)
go cter(out)
go sqer(out, in)
prter(in)
}
chan<- int
是一个只能发送的通道,可以发送但是不能接收;
<-chan int
是一个只能接收的通道,可以接收但是不能发送。
在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
九、如何优雅的从通道中循环取值?
channel 有一个特性:close关闭之后,在发送的时候会 panic,但是在接收的时候,是可以正常接收的。
这里介绍三种方式:
for range
for {}
select{}
for…range
通常使用 for range 的形式来循环取值。
func main(){
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
range c
产生的迭代值为Channel中发送的值,它会一直迭代直到channel被关闭。上面的例子中如果把close(c)
注释掉,程序会一直阻塞在for …… range
那一行。
for {} 死循环
我们还可以通过 for {} 死循环的形式,通过判断channel是否关闭来进行跳出循环进行取值。
func main(){
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for {
i, ok := <- c // 通道关闭后再取值ok=false
if !ok {
break;
}
fmt.Println(i)
}
fmt.Println("Finished")
}
select 语句
select
语句选择一组可能的send操作和receive操作去处理。它类似switch
,但是只是用来处理通讯(communication)操作。
它的case
可以是send语句,也可以是receive语句,亦或者default
。
receive
语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。
最多允许有一个default case
,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default
去处理,如果default case
存在的情况下。如果没有default case
,则select
语句会阻塞,直到某个case需要处理。
需要注意的是,nil channel上的操作会一直被阻塞,如果没有default case,只有nil channel的select会一直被阻塞。
select
语句和switch
语句一样,它不是循环,它只会选择一个case来处理,如果想一直处理channel,你可以在外面加一个无限的for循环:
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
select
有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。
下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select
设置为1秒超时,因此我们会打印出timeout 1
,而不是result 1
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
其实它利用的是time.After
方法,它返回一个类型为<-chan Time
的单向的channel,在指定的时间发送一个当前时间给返回的channel中。
十、Timer 和 Ticker
我们看一下关于时间的两个Channel。
timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值。下面的例子中第二行会阻塞2秒钟左右的时间,直到时间到了才会继续执行。
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")
当然如果你只是想单纯的等待的话,可以使用time.Sleep
来实现。
你还可以使用timer.Stop
来停止计时器。
timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
ticker
是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。下面的例子中ticker每500毫秒触发一次,你可以观察输出的时间。
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
类似timer, ticker也可以通过Stop
方法来停止。一旦它停止,接收者不再会从channel中接收数据了。
十一、Channel 异常情况总结
channel | nil | 非空 | 空的 | 满了 | 没满 |
---|---|---|---|---|---|
接收 | 阻塞 | 接收值 | 阻塞 | 接收值 | 接收值 |
发送 | 阻塞 | 发送值 | 发送值 | 阻塞 | 发送值 |
关闭 | panic | 关闭成功,读完数据后返回零值 | 关闭成功,返回零值 | 关闭成功,读完数据后返回零值 | 关闭成功,读完数据后返回零值 |
十二、 结束语
本篇文章介绍说明了:
什么是Channel?
Channel 类型怎么定义?
如何创建 Channnel ?
如何使用 Channel ?
Channel 的阻塞和非阻塞的定义和使用
Channel 如何使用单向通道?
如何优雅从通道中取值?
特殊的 Channel:Timer 和 Ticker
希望本篇文章对你有所帮助,谢谢。