先来温故一下channel
的概念:
Channels are a typed conduit through which you can send and receive values with the channel operator, <-.
一种管道类型,通过<-
操作符接收或发送数据,我们通过一个实例来了解一下管道的特性
启动两个线程, 一个输出 1,3,5,7…99, 另一个输出 2,4,6,8…100 最后 STDOUT 中按序输出 1,2,3,4,5…100
初看一下,很容易想到要用channel
,话不多说,先看代码
package main
import (
"sync"
)
var (
wg = sync.WaitGroup{}
signalOdd = make(chan bool, 1)
signalEven = make(chan bool, 1)
)
func odd() {
for i := 1; i <= 100; i += 2 {
if i != 1 {
<-signalEven
}
println(i)
signalOdd <- true
}
wg.Done()
}
func even() {
for i := 2; i <= 100; i += 2 {
<-signalOdd
println(i)
signalEven <- true
}
wg.Done()
}
func main() {
wg.Add(2)
go odd()
go even()
wg.Wait()
}
点击运行并查看执行结果
没有注释不要急,代码分析随后呈上。
从代码可以看到,这里有两个goroutine
,这两个goroutine
都有可能先执行,我们先来看其中一种
// odd先执行
func odd() {
for i := 1; i <= 10; i += 2 {
if i != 1 {
// 3. signalEven为空,等待写入,阻塞
// 9. signalEven不空,取出缓冲值
<-signalEven
}
// 1. 打印:1
// 10. 打印:3
println(i)
// 2. signalOdd为空,写入true
// 11. 同2,开始循环……
signalOdd <- true
}
wg.Done()
}
func even() {
for i := 2; i <= 10; i += 2 {
// 4. signalOdd不空,取出缓冲值
// 8. signalOdd为空,等待写入,阻塞
<-signalOdd
// 5. 打印:2
println(i)
// 6. signalEven为空,写入true
signalEven <- true
}
wg.Done()
}
环环相扣,看似简单,实则严谨,那如果是even
先执行呢?
func odd() {
for i := 1; i <= 100; i += 2 {
if i != 1 {
// 4. signalEven为空,等待写入,阻塞
<-signalEven
}
// 2. 打印:1
println(i)
// 3. signalOdd为空,写入true
signalOdd <- true
}
wg.Done()
}
func even() {
for i := 2; i <= 100; i += 2 {
// 1. signalOdd为空,等待写入,阻塞
// 5. signalOdd不空,取出缓冲值
// 8. 同1,开始循环……
<-signalOdd
// 6. 打印:2
println(i)
// 7. signalEven为空, 写入true
signalEven <- true
}
wg.Done()
}
所以两个goroutine
孰先孰后,因为有了signalOdd
和signalEven
两个管道的互相限制,最终都能得到有序的输出。关于管道的使用,归纳起来,有以下两点:
-
chan
为空,此时取数据会阻塞当前goroutine
的执行; -
chan
缓冲已满,此时写数据会阻塞当前goroutine
的执行
理解了以上两点,就能灵活运用chan
了(这里wg
主要作用是等待even
和odd
两个goroutine
执行完毕)。
上述分析过程让我想到了Go语言中一个最常见的也是经常被人提及的设计模式:不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存
显然,使用channel
的方式应归类于:通过通信的方式共享内存,这也是社区鼓励的方式,不过,若用共享内存的方式进行通信,在golang中又该如何实现,目前还未找到比较好的办法,后续找机会再补上。
Refer:
有疑问加站长微信联系(非本文作者)