channel
中文含义是通道,在Go程序中channel
的作用也就是通道,通道是用来传输数据的。那channel
是用来连通那两个地方的呢?答案是Goroutine,
不同的Goroutine
通过channel
相互传递信息(同一Goroutine
一般不用channel
传递或缓存信息)。那channel
不就是线程/协程同步工具了吗?是的,channel
自带同步功能,如:unbuffered channel
。 同时channel
也支持异步功能buffered channel
。在线程之间传递信息,很容易造成数据竞争
,不用担心,不同的Goroutine
同时发送或者接收数据,channel
都能保证数据发送完整和数据只能被一个Goroutine
完整接收。那么在使用channel
时有什么需要注意的呢?需要注意的是:
-
不要向一个已经关闭的
channel
发送数据,否则会造成panic;但可以向已经关闭的channel
接收数据。 -
向
nil channel
发送和接收数据都会阻塞。 -
使用
channel
时要从信号
的角度思考,不要把channel
仅仅当成数据队列
。 -
需要思考
阻塞
的问题,程序是否能承受channel
数据满,带来的发送和接收延迟。 -
需要考虑发送/接收谁先启动/关闭的问题。如果接收端先退出(不是使用close关闭
channel
,而是Goroutine退出),还使用unbuffered channel
,则会造成Goroutine泄露。此时使用buffer大小为1的channel
更加合适。
Unbuffered Channels
无缓冲的通道,即发送和接收是同时进行的,任何一端没有准备好,另一端都会处于阻塞状态。无缓冲的通道,用于Goroutine间信号同步是非常合适的,可以通过:
- 发送数据实时通知。
- 通过
close
通道达到一次通知的目的。
下面是一个使用无缓冲通道的例子,实现类似接力的效果:
package main
import (
"fmt"
"os"
"time"
)
func runner(baton chan int, over chan struct{}) {
if baton == nil || over == nil {
fmt.Println("baton or over channel is nil, terminate the program")
os.Exit(-1)
}
count := <-baton
fmt.Printf("runner %d is running...\n", count)
time.Sleep(time.Second)
if count == 4 {
fmt.Println("runner 4 reach the endpoint")
over <- struct{}{}
} else {
fmt.Printf("runner %d give the baton to the runner %d\n", count, count+1)
baton <- count + 1
}
}
func main() {
baton := make(chan int)
over := make(chan struct{})
fmt.Println("four runners on the Line")
for i := 0; i < 4; i++ {
go runner(baton, over)
}
fmt.Println("start the race")
baton <- 1
<-over
fmt.Println("The race is over")
}
输出:
four runners on the Line
start the race
runner 1 is running...
runner 1 give the baton to the runner 2
runner 2 is running...
runner 2 give the baton to the runner 3
runner 3 is running...
runner 3 give the baton to the runner 4
runner 4 is running...
runner 4 reach the endpoint
The race is over
Buffered Channels
有缓冲的通道,给人的感觉像是Goroutine间的队列,用于缓冲数据。的确,有缓冲的通道可以用于这个目的,但作为数据缓冲通道使用时需要注意:
- 清楚缓冲通道是有大小限制的。即,通道满后,发送端无法在向通道发送数据,进入阻塞状态。所以在使用通道时,能够考虑到阻塞带来的后果。
- 缓冲通道的大小不易过大,因为会在
make
时就一次分配好内存,可能会占用大量内存。
数据通道
比较适用的场景有:
生产者消费者
模型。此时的结果更注重于消费者
,生产者
处于阻塞状态并不会造成性能上的损失。如:从多个文件中搜索关键字的功能,此时通道中存放的是文件的路径,程序的效率完成体现在消费者
的执行速率上,而生产者
在通道满时处于阻塞状态并不会带来性能上的下降。- 一开始能确定数据通道大小的场景。那么发送端就不会处于阻塞状态,效率取决与接收端。
有缓冲的通道也可以作为信号通道
使用,不过作为信号通道使用时需要在一开始就能确定通道大小。接收端通过获取一个数据作为信号,执行开始或关闭操作。下面看一个简单的例子:
package main
import (
"time"
)
func main() {
const count = 5
sch := make(chan struct{}, count)
ech := make(chan struct{}, count)
for i := 0; i < count; i++ {
go func(sch, ech chan struct{}) {
<-sch
time.Sleep(time.Second)
ech <- struct{}{}
}(sch, ech)
}
for i := 0; i < count; i++ {
sch <- struct{}{}
}
index := count
for _ = range ech {
index -= 1
if index == 0 {
break
}
}
}
上面的例子通过两个通道,sch
作为开始信号通道,告诉子Goroutine开始执行。ech
作为结束信号通道,告诉主Goroutine子任务结束。
One Buffered Channels
通道大小为1的缓冲通道为什么单独拿出来说呢?因为在某些情况下有其适用的场景。在接收端先于发送端退出的情况下,如果通道是无缓冲的,那么发送端就会阻塞在发送操作上,造成Goroutine泄露。下面来看一个例子吧。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan struct{})
var stop chan struct{} = nil
go func(ch chan struct{}) {
defer func() {
if err := recover(); err != nil {
fmt.Println("Catch panic: ", err)
}
}()
func(ch chan struct{}) {
panic("expect receiver error")
<-ch
fmt.Println("Receiver exit...")
}(ch)
}(ch)
go func(ch chan struct{}) {
fmt.Println("Sender before send...")
time.Sleep(time.Second)
ch <- struct{}{}
fmt.Println("Sender exit...")
}(ch)
<-stop
}
输出:
Sender before send...
Catch panic: expect receiver error
fatal error: all goroutines are asleep - deadlock!
...
上面造成deadlock
是正常结果,这里要看的是,Reciever
由于panic
异常退出时,导致Sender
阻塞在发送语句上,Goroutine泄露。而将通道大小改为1,则不会造成Goroutine泄露。