Glang之通道

通道

Go 语言里,你不仅可以使用原子函数和互斥锁来保证对共享资源的安全访
问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在 goroutine 之间做
同步。
当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了
确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享
内置类型、命名类型、结构类型和引用类型的值或者指针。
Go 语言中需要使用内置函数 make 来创建一个通道
// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)
可以看到使用内置函数 make 创建了两个通道,一个无缓冲的通道,
一个有缓冲的通道。 make 的第一个参数需要是关键字 chan ,之后跟着允许通道交换的数据的
类型。如果创建的是一个有缓冲的通道,之后还需要在第二个参数指定这个通道的缓冲区的大小。
向通道发送值或者指针需要用到 <- 操作符
// 有缓冲的字符串通道
buffered := make(chan string, 10)
// 通过通道发送一个字符串
buffered <- "Gopher"
我们创建了一个有缓冲的通道,数据类型是字符串,包含一个 10 个值
的缓冲区。之后我们通过通道发送字符串 "Gopher" 。为了让另一个 goroutine 可以从该通道里接
收到这个字符串,我们依旧使用 <- 操作符,但这次是一元运算符,
// 从通道接收一个字符串
value := <-buffered
当从通道里接收一个值或者指针时, <- 运算符在要操作的通道变量的左侧,如代码清单 6-19
所示。
通道是否带有缓冲,其行为会有一些不同。理解这个差异对决定到底应该使用还是不使用缓
冲很有帮助。下面我们分别介绍一下这两种类型。
 
 

无缓冲的通道

无缓冲的通道 unbuffered channel )是指在接收前没有能力保存任何值的通道。这种类型的通
道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine
没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。这种对通道进行发送
和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
 
 
在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收。在第 2 步,左侧
goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在
通道中被锁住,直到交换完成。在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通
道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成。在第 4 步和第 5 步,
进行交换,并最终,在第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住
goroutine 得到释放。两个 goroutine 现在都可以去做别的事情了。
为了讲得更清楚,让我们来看两个完整的例子。这两个例子都会使用无缓冲的通道在两个
goroutine 之间同步交换数据。
在网球比赛中,两位选手会把球在两个人之间来回传递。选手总是处在以下两种状态之一:
要么在等待接球,要么将球打向对方。可以使用两个 goroutine 来模拟网球比赛,并使用无缓冲
的通道来模拟球的来回,
 
 
 // 这个示例程序展示如何用无缓冲的通道来模拟
 // 2 个 goroutine 间的网球比赛
 package main

 import ( 
 "fmt"
 "math/rand"
 "sync"
 "time"
 ) 

 // wg 用来等待程序结束
 var wg sync.WaitGroup

 func init() { 
 rand.Seed(time.Now().UnixNano())
 } 

 // main 是所有 Go 程序的入口
 func main() { 
 // 创建一个无缓冲的通道
 court := make(chan int)

 // 计数加 2,表示要等待两个 goroutine
 wg.Add(2)

 // 启动两个选手
 go player("Nadal", court)
 go player("Djokovic", court)

 // 发球
 court <- 1 

 // 等待游戏结束
 wg.Wait()
 } 

 // player 模拟一个选手在打网球
 func player(name string, court chan int) { 
 // 在函数退出时调用 Done 来通知 main 函数工作已经完成
 defer wg.Done()

 for { 
 // 等待球被击打过来
 ball, ok := <-court
 if !ok { 
 // 如果通道被关闭,我们就赢了
 fmt.Printf("Player %s Won\n", name)
 return
 } 

 // 选随机数,然后用这个数来判断我们是否丢球
 n := rand.Intn(100)
 if n%13 == 0 { 
 fmt.Printf("Player %s Missed\n", name)

 // 关闭通道,表示我们输了
 close(court)
 return
 } 

 // 显示击球数,并将击球数加 1 
 fmt.Printf("Player %s Hit %d\n", name, ball)
 ball++

 // 将球打向对手
 court <- ball
 } 
 }
运行这个程序会得到代码   所示的输出
 
Player Nadal Hit 1
Player Djokovic Hit 2
Player Nadal Hit 3
Player Djokovic Missed
Player
Nadal Won
 
在这两个例子里,我们使用无缓冲的通道同步 goroutine ,模拟了网球。代码的流程
与这两个活动在真实世界中的流程完全一样,这样的代码很容易读懂。现在知道了无缓冲的通道
是如何工作的,接下来我们会学习有缓冲的通道的工作方法
 

有缓冲的通道

有缓冲的通道 buffered channel )是一种在被接收前能存储一个或者多个值的通道。这种类
型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的
条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲
区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大
的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的
通道没有这种保证。
在图 中可以看到两个 goroutine 分别向有缓冲的通道里增加一个值和从有缓冲的通道里移
除一个值。在第 1 步,右侧的 goroutine 正在从通道接收一个值。在第 2 步,右侧的这个 goroutine
独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里。在第 3 步,左侧的
goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的
两个操作既不是同步的,也不会互相阻塞。最后,在第 4 步,所有的发送和接收都完成,而通道
里还有几个值,也有一些空间可以存更多的值。
让我们看一个使用有缓冲的通道的例子,这个例子管理一组 goroutine 来接收并完成工作。
有缓冲的通道提供了一种清晰而直观的方式来实现这个功能,
 // 这个示例程序展示如何使用
 // 有缓冲的通道和固定数目的
 // goroutine 来处理一堆工作
 package main

 import ( 
 "fmt"
 "math/rand"
 "sync"
 "time"
 ) 

 const ( 
 numberGoroutines = 4 // 要使用的 goroutine 的数量
 taskLoad = 10 // 要处理的工作的数量
 ) 

 // wg 用来等待程序完成
 var wg sync.WaitGroup

 // init 初始化包,Go 语言运行时会在其他代码执行之前
 // 优先执行这个函数
 func init() { 
 // 初始化随机数种子
 rand.Seed(time.Now().Unix())
 } 

 // main 是所有 Go 程序的入口
 func main() { 
 // 创建一个有缓冲的通道来管理工作
 tasks := make(chan string, taskLoad)

 // 启动 goroutine 来处理工作
 wg.Add(numberGoroutines)
 for gr := 1; gr <= numberGoroutines; gr++ { 
 go worker(tasks, gr)
 } 

 // 增加一组要完成的工作
 for post := 1; post <= taskLoad; post++ { 
 tasks <- fmt.Sprintf("Task : %d", post)
 } 

 // 当所有工作都处理完时关闭通道
 // 以便所有 goroutine 退出
 close(tasks)

 // 等待所有工作完成
 wg.Wait()
 } 

 // worker 作为 goroutine 启动来处理
 // 从有缓冲的通道传入的工作
 func worker(tasks chan string, worker int) { 
 // 通知函数已经返回
 defer wg.Done()

 for { 
 // 等待分配工作
 task, ok := <-tasks
 if !ok { 
 // 这意味着通道已经空了,并且已被关闭
 fmt.Printf("Worker: %d : Shutting Down\n", worker)
 return
 } 

 // 显示我们开始工作了
 fmt.Printf("Worker: %d : Started %s\n", worker, task)

 // 随机等一段时间来模拟工作
 sleep := rand.Int63n(100)
 time.Sleep(time.Duration(sleep) * time.Millisecond)

 // 显示我们完成了工作
 fmt.Printf("Worker: %d : Completed %s\n", worker, task)
 } 
 }
listing24.go
Worker: 1 : Started Task : 1
Worker: 2 : Started Task : 2
Worker: 3 : Started Task : 3
Worker: 4 : Started Task : 4
Worker: 1 : Completed Task : 1
Worker: 1 : Started Task : 5
Worker: 4 : Completed Task : 4
Worker: 4 : Started Task : 6
Worker: 1 : Completed Task : 5
Worker: 1 : Started Task : 7
Worker: 2 : Completed Task : 2
Worker: 2 : Started Task : 8
Worker: 3 : Completed Task : 3
Worker: 3 : Started Task : 9
Worker: 1 : Completed Task : 7
Worker: 1 : Started Task : 10
Worker: 4 : Completed Task : 6
Worker: 4 : Shutting Down
Worker: 3 : Completed Task : 9
Worker: 3 : Shutting Down
Worker: 2 : Completed Task : 8 Worker: 2 : Shutting Down
Worker: 1 : Completed Task : 10
Worker:1 : Shutting Down
 
由于程序和 Go 语言的调度器带有随机成分,这个程序每次执行得到的输出会不一样。不过,
通过有缓冲的通道,使用所有 4 goroutine 来完成工作,这个流程不会变。从输出可以看到每
goroutine 是如何接收从通道里分发的工作。
main 函数的第 31 行,创建了一个 string 类型的有缓冲的通道,缓冲的容量是 10 。在
34 行,给 WaitGroup 赋值为 4 ,代表创建了 4 个工作 goroutine 。之后在第 35 行到第 37 行,
创建了 4 goroutine ,并传入用来接收工作的通道。在第 40 行到第 42 行,将 10 个字符串发送
到通道,模拟发给 goroutine 的工作。一旦最后一个字符串发送到通道,通道就会在第 46 行关闭,
main 函数就会在第 49 行等待所有工作的完成。
46 行中关闭通道的代码非常重要。当通道关闭后, goroutine 依旧可以从通道接收数据,
但是不能再向通道里发送数据。能够从已经关闭的通道接收数据这一点非常重要,因为这允许通
道关闭后依旧能取出其中缓冲的全部值,而不会有数据丢失。从一个已经关闭且没有数据的通道
里获取数据,总会立刻返回,并返回一个通道类型的零值。如果在获取通道时还加入了可选的标
志,就能得到通道的状态信息。
worker 函数里,可以在第 58 行看到一个无限的 for 循环。在这个循环里,会处理所有
接收到的工作。每个 goroutine 都会在第 60 行阻塞,等待从通道里接收新的工作。一旦接收到返
回,就会检查 ok 标志,看通道是否已经清空而且关闭。如果 ok 的值是 false goroutine 就会
终止,并调用第 56 行通过 defer 声明的 Done 函数,通知 main 有工作结束。
如果 ok 标志是 true ,表示接收到的值是有效的。第 71 行和第 72 行模拟了处理的工作。
一旦工作完成, goroutine 会再次阻塞在第 60 行从通道获取数据的语句。一旦通道被关闭,这个
从通道获取数据的语句会立刻返回, goroutine 也会终止自己。
有缓冲的通道和无缓冲的通道的例子很好地展示了如何编写使用通道的代码。在下一章,我
们会介绍真实世界里的一些可能会在工程里用到的并发模式。
 
listing24.go
listing24.go
listing24.go
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

猜你喜欢

转载自blog.csdn.net/youandme520/article/details/113536511