原子函数和互斥锁都能工作,但是依靠它们都不会让编写并发程序变得更简单,更不容易出错,或者更有趣。在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
1. 无缓冲的通道
无缓冲的通道是指在接收前没有能力保存任何值得通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
下面通过一个例子,这两个例子都会使用无缓冲的通道在两个goroutine之间同步交换数据。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func init(){
rand.Seed(time.Now().UnixNano())
}
func main(){
court:=make(chan int)
wg.Add(2)
//启动两个选手
go player("Nadal",court)
go player("Djokovic",court)
court<-1 //发球
wg.Wait()
}
func player(name string,court chan int){
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
}
fmt.Printf("Player %s Hit %d\n",name,ball)
ball++
//将球打向对手
court<-ball
}
}
首先创建了一个int型的无缓冲的通道,让两个goroutine在击球时能够互相同步。之后创建了参与比赛的两个goroutine。在这个时候,两个goroutine都阻塞住等待击球。将球发到通道里,程序开始执行这个比赛,直到某个goroutine输掉比赛。
另一个例子,用不同的模式,使用无缓冲的通道,在goroutine之间同步数据,来模拟接力比赛。
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
baton:=make(chan int)
wg.Add(1) //为最后一位跑步者将计数加1
go Runner(baton) //第一位跑步者持有接力棒
baton<-1 //开始比赛
wg.Wait()
}
func Runner(baton chan int){
var newRunner int
runner:=<-baton //等待接力棒
fmt.Printf("Runner %d Running With Baton\n",runner)
//创建下一位跑步者
if runner!=4{
newRunner=runner+1
fmt.Printf("Runner %d To The Line\n",newRunner)
go Runner(baton)
}
time.Sleep(1*time.Second)
if runner==4{
fmt.Printf("Runner %d Finished,Race Over\n",runner)
wg.Done()
return
}
fmt.Printf("Runner %d Exchange With Runner %d\n",runner,newRunner)
baton<-newRunner
}
在Runner goroutine里,可以看到接力棒baton是如何在跑步者之间传递的。goroutine对baton通道执行接收操作,表示等候接力棒。一旦接力棒传了进来,就会创建一位新跑步者,准备接力下一棒,直到goroutine是第四个跑步者。跑步者围绕跑到跑100ms。如果第四个跑步者完成了比赛,就调用Done,将WaitGroup减1,之后goroutine返回。如果这个goroutine不是第四个跑步者,那么接力棒会交到下一个已经在等待的跑步者手里,这个时候,goroutine会被锁住,直到交接完成。
2. 有缓冲的通道
有缓冲的通道是一种在被接收前能存储一个或者多个值得通道。这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进行数据交换;有缓冲的通道没有这种保证。
下面这个例子管理一组goroutine来接收并完成工作。有缓冲的通道提供了一种清晰而直观的方式来实现这个功能。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
const(
numberGoroutines=4 //要使用的goroutine的数量
taskLoad=10 //要处理的工作的数量
)
var wg sync.WaitGroup
func init(){
rand.Seed(time.Now().Unix()) //初始化随机数种子
}
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)
}
close(tasks)
wg.Wait()
}
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)
}
}
其中关闭通道的代码非常重要。当通道关闭后,goroutine依旧可以从通道接收数据,但是不能再向通道里发送数据。能够从已经关闭的通道接收数据这一点非常重要,因为这允许通道关闭后依旧能取出其中缓冲的全部值,而不会有数据丢失。从一个已经关闭且没有数据的通道里获取数据,总会立刻返回,并返回一个通道类型的零值。如果在获取通道时还加入了可选的标志,就能得到通道的状态信息。
在worker函数里,可以看到一个无限for循环。在这个循环里,会处理所有接收到的工作。每个goroutine都会阻塞,等待从通道里接收新的工作。一旦接收到返回,就会检查ok标志,看通道是否已经清空而且关闭。