今天这篇笔记我们来记录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 <- ifmt.Printf("writer %d \n", i)}}()for integer := range intStream {fmt.Printf("receive %v \n", integer)}fmt.Println(<-intStream)fmt.Println(<-intStream)

我们写入了五个value到intStream里面, 读取的时候通过range我就不用知道这个次数了,通过for range 就都拿到了。 上面程序输出结果如下:

writer 1 receive 1 receive 2writer 2writer 3receive 3receive 4writer 4writer 5receive 500

结果比较有意思, 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 <- ifmt.Printf("Sending: %d \n", i)}}()time.Sleep(10 * time.Second)for i := 0; i < 5; i++ {v := <-intStreamfmt.Printf("Received: %d \n", v)time.Sleep(1 * time.Second)}

定义一个缓冲区为2的channel, 当写入两个后会被阻塞。 输出结果如下

Sending: 0 Sending: 1 Received: 0 Sending: 2Received: 1 Sending: 3 Received: 2 Sending: 4 Producer DoneReceived: 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 ... \n")select {case <-c:fmt.Printf("Unblocked %v later. \n", 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 ... \n")loop:for {select {case <-c:fmt.Printf("Unblocked %v later. \n", time.Since(start))break loopcase data := <-c2:fmt.Printf("C2 received %d,  %v later. \n", 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 intfor i := 1000; i >= 0; i-- {select {case <-c1:c1Count++case <-c2:c2Count++}}fmt.Printf("c1Count:%d \nc2Count: %d \n", 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. \n", 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. \n", time.Since(start))}

程序几乎立刻执行了default, 输出如下
default after 0s later.