2.7.1 channel通信与CSP并发模型
在linux系统编程中,有⼀种进程间通信的⽅式叫管道,两个进程可以借助内核开辟的缓冲区进⾏数据交
换,形象上就像是⼀个⽔管(内核的缓冲区)把数据从⼀个进程流向另外⼀个进程。在Go语⾔当中,也
设计了⼀款类似的通信⽅式 – channel,利⽤channel读写的特性,不光可以实现Goroutine之间精准通
信,也可以控制Goroutine之间的同步协调。
这个并发模型就是著名的CSP(Communicating Sequential Process),这个模型最早是上世纪70年代
提出的。
在Go语⾔之中,我们借助内置make函数创建channel,channel的创建可以有缓冲区,也可以⽆缓冲
区。
make(chan chantype)
make(chan chantype, 5)
对于通道,我们关键是掌握他们的读写⾏为。
缓冲区⽆数据(⽆缓冲区时写端未写数据),读阻塞直到写端有数据写⼊ 缓冲区有数据,顺利读数据,
结束
package main
import (
"fmt"
"time"
)
var c chan string
func reader() {
msg := <-c //读通道
fmt.Println("I am reader,", msg)
}
func main() {
c = make(chan string)
go reader()
fmt.Println("begin sleep")
time.Sleep(time.Second * 3) //睡眠3s为了看执⾏效果
c <- "hello" //写通道
time.Sleep(time.Second * 1) //睡眠1s为了看执⾏效果
}
begin sleep
I am reader, hello
我们来是实现⼀个通过goroutine实现数字传递的例⼦,goroutine1循
环将1,2,3,4,5传递给goroutine2,goroutine2负责将数字平⽅后传递给goroutine3,goroutine3
负责打印接收到的数字。
分析该应⽤,我们需要⾄少2个channel,3个goroutine,其中main函数可以直接是第三个goroutine,
所以再创建2个就够了。
package main
import (
"fmt"
"time"
)
var c1 chan int
var c2 chan int
func main() {
c1 = make(chan int)
c2 = make(chan int)
//counter
go func() {
for i := 0; i < 10; i++ {
c1 <- i //向通道c1写⼊数据
time.Sleep(time.Second * 1)
}
}()
//squarer
go func() {
for {
num := <-c1 //读c1数据
c2 <- num * num //将平⽅写⼊c2
}
}()
//printer
for {
num := <-c2
fmt.Println(num)
}
}
0
1
4
9
16
25
36
49
64
81
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
C:/Users/nlp_1/goWorkspace/src/main.go:30 +0xac
goroutine 7 [chan receive]:
main.main.func2()
C:/Users/nlp_1/goWorkspace/src/main.go:24 +0x28
created by main.main in goroutine 1
C:/Users/nlp_1/goWorkspace/src/main.go:22 +0x92
这样执⾏完效果不太好,因为当第⼀个goroutine执⾏输出10个后,后⾯没有goroutine向通道写数据,
这样就会出现Go语⾔不允许的情况,这种错误当然也是可预⻅的,就是代表进程被锁死了,所以Go语
⾔给定义的错误是Deadlock(死锁)。
这是由于channel的知识点我们还需要知道,通道可以创建,也可以关闭,在读取的时候,也可以使⽤
指示器变量来判断有没有问题,顺便提⼀下range在这⾥仍然可以读取channel,此时不需要“<-”。我们
来尝试结束后关闭channel,然后优雅的结束整个进程。
package main
import (
"fmt"
"time"
)
var c1 chan int
var c2 chan int
func main() {
c1 = make(chan int)
c2 = make(chan int)
//counter
go func() {
for i := 0; i < 10; i++ {
c1 <- i //向通道c1写⼊数据
time.Sleep(time.Second * 1)
}
close(c1) //关闭c1
}()
//squarer
go func() {
for {
num, ok := <-c1 //读c1数据
if !ok {
break
}
c2 <- num * num //将平⽅写⼊c2
}
close(c2) //关闭c2
}()
//printer
for {
num, ok := <-c2
if !ok {
break
}
fmt.Println(num)
}
}
特别注意,对通道的读写操作都会使goroutine阻塞,通道的关闭应该由写端来操作。此外,channel也
可以作为函数参数,默认情况下⼀个channel是读写都可以的,为了防⽌不该写的goroutine发⽣写⾏
为,Go语⾔设计了channel传递给函数的时候可以指定为单⽅向,读或者写!⽽这个单⽅向表述⾮常明
确:
chan_name chan<- chan_type //只写通道
chan_name <-chan chan_type //只读通道
我们将上述的例⼦改造,因为三个goroutine对channel的操作就是读或者写。
//counter,对c1只写
go func(out chan<- int) {
for i := 0; i < 10; i++ {
out <- i //向通道c1写⼊数据
time.Sleep(time.Second * 1)
}
close(out)
}(c1)
//squarer,对c1只读,对c2只写
go func(in <-chan int, out chan<- int) {
for {
num, ok := <-in //读c1数据
if !ok {
break
}
out <- num * num //将平⽅写⼊c2
}
close(out)
}(c1, c2)
2.7.2 定时器
接下来我们来实现⼀个⽕箭发射的例⼦,准备⼀个倒数计时5秒,然后打印⼀个发射。当然这个例⼦可
以⽤Sleep来控制每隔1s计数⼀次,不过在这⾥我们使⽤Go语⾔为我们提供的定时器来做这件事,定时
器的关键也是channel。
在time包中存在⼀个NewTimer,传⼊⼀个时间间隔n,获得⼀个Timer,Timer结构体中包含了⼀个
C,这是⼀个通道类型,于是在时间n之后,C中会被写⼊时间戳。
package main
import (
"fmt"
"time"
)
func launch() {
fmt.Println("发射!")
}
func main() {
ticker := time.NewTicker(time.Second)
num := 5
for {
<-ticker.C //读取⽆⼈接收
fmt.Println(num)
num--
if num == 0 {
break
}
}
ticker.Stop()
launch() //发射⽕箭
}
5
4
3
2
1
发射!!
这样可以实现⽕箭发射的功能,不过如果临时想取消发射,该如何做呢?按ctrl+c的⽅式太简单粗暴了
⼀下,⽐如想要按下任意键取消发射呢?
2.7.3 多路channel监控
我们很⾃然想到读标准输⼊就可以了,甚⾄也会⽴刻想到启动⼀个goroutine去监听标准输⼊,如果有
输⼊,⽴即退出进程。
func cancel() {
data := make([]byte, 10)
os.Stdin.Read(data) //读标准输⼊
os.Exit(1) //退出进程
}
我们可以实现这样⼀个函数,读取标准输⼊,但是这样退出整个进程也不太优雅,我们还是想⽐较稳妥
的退出。于是很多⼈想到,我们可以在建⽴⼀个channel,当标准输⼊有数据的时候,将数据写⼊该
channel,在main函数中监控该channel,如果读到数据,则不执⾏后⾯的发射,直接return。但是问
题来了,通道读都是阻塞的,我们的⽕箭发射还怎么做呢?在linux下我们知道多路IO监控可以使⽤
select、poll、epoll等,在Go语⾔⾥,同样提供了⼀个机制对多路channel监控,这个机制的关键就是
select-case语句。
select 可以这也监控多个通道,当任⼀通道有数据写⼊时,select都会⽴即返回解除阻塞。完整代码如
下:
package main
import (
"fmt"
"os"
"time"
)
// 监控标准输入
func cancel(out chan<- string) {
buf := make([]byte, 10)
os.Stdin.Read(buf) //阻塞读
// 通道通知主控goroutine
out <- "stop"
}
func main() {
stdin_chan := make(chan string)
go cancel(stdin_chan)
ticker := time.NewTicker(time.Second)
num := 5
for num > 0 {
// select 可以监控多路channel 任意channel有数据写入 立即返回
select {
case <-ticker.C:
fmt.Println(num)
num--
case <-stdin_chan:
ticker.Stop()
return
}
}
fmt.Println("发射!!")
ticker.Stop()
}