《Go 语言并发之道》读书笔记(五)

《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.

hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 《Go 语言并发之道》读书笔记(五)