《Go 语言并发之道》读书笔记(五)
今天这篇笔记我们来记录Channel 和 Select, Go语言并发中Channel是goroutine传递数据的桥梁,是非常重要的一个工具。
定义Channel
双向Channel
要定义一个channel很简单,只需要在类型前面加上chan就可以了,
stringStream := make(chan string)
这样就是定义和实例化了一个string 类型的双向channel,
先来看一个Hello World的例子
func main() {
stringStream := make(chan string)
go func() {
stringStream <- "Hello channels"
}()
fmt.Println(<-stringStream)
运行代码控制台打印出“Hello channels”, 这个简单的例子中我们定义了一个string类型的channel, 启动一个goroutine, 往这个channel中写入“Hello channels”, 主的goroutinue会读取这个channel里面的value, 读取是阻塞的,如果我们把写的代码注释掉“stringStream <- “Hello channels””,程序运行会报死锁,因为没有谁会写入了,它一直等待。
单向Channel
我们也可以声明单向的channel,也就是只读或者只写的channel.
var receiveChan <-chan string //一个只读的channel
var sendChan chan<- string //一个只写的channel,
既然是只读,那么谁来给它写入呢, 这里其实还是需要一个双向的channel,然后把双向的channel赋值给单向channel,如
stringStream := make(chan string)
receiveChan = stringStream
sendChan = stringStream
只读和只写channel有什么作用呢? 他们主要是用在方法的参数或者返回中,用户看到这个chan是只读的或者只写的就明确了它的使用方法。 对于只读的,我们实际上用个双向的channel,然后写入双向channel后, 把双向channel赋值给只读的channel. 如下示例代码
func main() {
stringStream := make(chan string)
go send(stringStream, "passed message")
receive(stringStream)
}
func send(pings chan<- string, msg string) {
fmt.Println("ping " + msg)
pings <- msg
}
func receive(receiver <-chan string) {
fmt.Println(<-receiver)
}
我们在send方法中知道pings是只写的,不会读取它
在receive方法中知道receiver是只读的, 不会写它
读取和写入Channel
上面例子我们一件看到读就通过value := <-channel, 把channel中的数据读出来, 写就通过 channel<- value, 箭头方向也比较明确,比较好理解。这里再给个通过range读取channel的方法
先看示例代码
intStream := make(chan int)
go func() {
defer close(intStream)
for i := 1; i <= 5; i++ {
intStream <- i
fmt.Printf("writer %d
", i)
}
}()
for integer := range intStream {
fmt.Printf("receive %v
", integer)
}
fmt.Println(<-intStream)
fmt.Println(<-intStream)
我们写入了五个value到intStream里面, 读取的时候通过range我就不用知道这个次数了,通过for range 就都拿到了。 上面程序输出结果如下:
writer 1
receive 1
receive 2
writer 2
writer 3
receive 3
receive 4
writer 4
writer 5
receive 5
0
0
结果比较有意思, receive 2 跑到writer3的前面去了, 我猜测是这个channel是阻塞的,写入的时候,必须读了才能再写,读到1以后,2就可以写了,还没有来得及打印writer, read就拿到了。所以感觉上receive跑到writer前面去了。
最后两个00是我故意打印出来的,从关闭的channel也能拿到有返回的数据,如果想确定数据是不是正常写入的,可以加上 value,ok := <- intStream, 判断 ok 是true和false判断是否是正常写入的。
缓冲Channel
我们前面看到的例子,写入数据到channel后,必须等别的goroutine读到后才可以继续写,那么如果我想写入后继续去干别的,就需要用到缓冲Channel, 也就可以多写几个到channel。 如下示例代码
intStream := make(chan int, 2)
go func() {
defer close(intStream)
defer fmt.Println("Producer Done")
for i := 0; i < 5; i++ {
intStream <- i
fmt.Printf("Sending: %d
", i)
}
}()
time.Sleep(10 * time.Second)
for i := 0; i < 5; i++ {
v := <-intStream
fmt.Printf("Received: %d
", v)
time.Sleep(1 * time.Second)
}
定义一个缓冲区为2的channel, 当写入两个后会被阻塞。 输出结果如下
Sending: 0
Sending: 1
Received: 0
Sending: 2
Received: 1
Sending: 3
Received: 2
Sending: 4
Producer Done
Received: 3
Received: 4
可以看到当发送了两个后,发送就阻塞起来了,直到读取了之后,才可以继续发送。
这里有个疑问点作者说 make(chan int) 和 make(chan int, 0) 是等效的,我实际验证效果也确实是一样的,但是我想不应该是make(chan int, 1) 吗?但是实际效果make(chan int, 1) 和make(chan int, 0) 确实不一样。 我实验了下,make(chan int, 1) 写第二个的时候被阻塞,用
make(chan int, 0),写一开始就会阻塞,直到开始读了,写才会成功。当然不是先写后读,只是一种相互的阻塞状态。
Select
作者在书中写道:“Select是一个具有并发性的Go语言最重要的事情之一, 在一个系统中两个或者多个组件的交集中,可以在本地、单个函数、或者类型以及全局范围内查找select语句绑定在一起的channel。除了连接组件之外,在程序的某些关键节点上, select 语句可以帮助安全地将channel与诸如取消、超时、等待、默认值之类的概率结合起来”。
单一channel select
先来看一个简单的例子
start := time.Now()
c := make(chan interface{})
go func() {
time.Sleep(5 * time.Second)
close(c)
}()
fmt.Print("Blocking on read ...
")
select {
case <-c:
fmt.Printf("Unblocked %v later.
", time.Since(start))
}
程序输出结果如下, 等待5S后,关闭了channel, 阻塞结束。
Blocking on read ...
Unblocked 5.0101794s later.
上面是一个单一channel select的例子, 它等效于下面的语句
if c == nil {
block()
}
<- c
多个channel
接着我们看一个多个channel可用的例子, 我自己稍微改装了一下上面的例子
start := time.Now()
c := make(chan interface{})
c2 := make(chan int)
go func() {
time.Sleep(5 * time.Second)
for i := 0; i < 3; i++ {
c2 <- i
}
close(c)
}()
fmt.Print("Blocking on read ...
")
loop:
for {
select {
case <-c:
fmt.Printf("Unblocked %v later.
", time.Since(start))
break loop
case data := <-c2:
fmt.Printf("C2 received %d, %v later.
", data, time.Since(start))
}
}
这里有两个case, 一个收到后会退出循环,一个会读取channel里面的数据,程序运行结果如下所示
Blocking on read ...
C2 received 0, 5.0148217s later.
C2 received 1, 5.0155568s later.
C2 received 2, 5.0161642s later.
Unblocked 5.0167839s later.
我们写入的3个数据都被读取到了,并且关闭channel后退出了循环。
书中还列举了一个当多个channel都可用的时候,Go 语言执行伪随机选择,
c1 := make(chan interface{})
close(c1)
c2 := make(chan interface{})
close(c2)
var c1Count, c2Count int
for i := 1000; i >= 0; i-- {
select {
case <-c1:
c1Count++
case <-c2:
c2Count++
}
}
fmt.Printf("c1Count:%d
c2Count: %d
", c1Count, c2Count)
程序运行结果如下
c1Count:483
c2Count: 518
运行1000次,两个case比较平均的执行
超时
我们来看一个超时的例子
start := time.Now()
c1 := make(chan interface{})
select {
case <-c1:
fmt.Println("received c1.")
case <-time.After(2 * time.Second):
fmt.Printf("Timed out. after %v later.
", time.Since(start))
}
程序输出
Timed out. after 2.0099758s later.
没有程序写入c1, 所以在等待2S后,执行了time out.
default
来看default的例子
start := time.Now()
var c1, c2 <-chan interface{}
select {
case <-c1:
fmt.Println("received c1.")
case <-c2:
fmt.Println("received c2.")
default:
fmt.Printf("default after %v later.
", time.Since(start))
}
程序几乎立刻执行了default, 输出如下
default after 0s later.