你极可能从某种途径据说过 Go 语言。它愈来愈受欢迎,而且有充分的理由能够证实。 Go 快速、简单,有强大的社区支持。学习这门语言最使人兴奋的一点是它的并发模型。 Go 的并发原语使建立多线程并发程序变得简单而有趣。我将经过插图介绍 Go 的并发原语,但愿能点透相关概念以方便后续学习。本文是写给 Go 语言编程新手以及准备开始学习 Go 并发原语 (goroutines 和 channels) 的同窗。编程
你可能已经写过一些单线程程序。一个经常使用的编程模式是组合多个函数来执行一个特定任务,而且只有前一个函数准备好数据,后面的才会被调用。多线程
首先咱们将用上述模式编写第一个例子的代码,一个描述挖矿的程序。它包含三个函数,分别负责执行寻矿、挖矿和练矿任务。在本例中,咱们用一组字符串表示 rock
(矿山) 和 ore
(矿石),每一个函数都以它们做为输入,并返回一组 “处理过的” 字符串。对于一个单线程的应用而言,该程序可能会按以下方式来设计:并发
它有三个主要的函数:finder、miner 和 smelter。该版本的程序的全部函数都在单一线程中运行,一个接着一个执行,而且这个线程 (名为 Gary 的 gopher) 须要处理所有工做。函数
func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} foundOre := finder(theMine) minedOre := miner(foundOre) smelter(minedOre) }
在每一个函数最后打印出 "ore" 处理后的结果,获得以下输出:学习
From Finder: [ore ore ore] From Miner: [minedOre minedOre minedOre] From Smelter: [smeltedOre smeltedOre smeltedOre]
这种编程风格具备易于设计的优势,可是当你想利用多个线程并执行彼此独立的函数时会发生什么呢?这就是并发程序设计发挥做用的地方。ui
这种设计使得 “挖矿” 更高效。如今多个线程 (gophers) 是独立运行的,从而 Gary 再也不承担所有工做。其中一个 gopher 负责寻矿,一个负责挖矿,另外一个负责练矿,这些工做可能同时进行。this
为了将这种并发特性引入咱们的代码,咱们须要建立独立运行的 gophers 的方法以及它们之间彼此通讯 (传送矿石) 的方法。这就须要用到 Go 的并发原语:goroutines 和 channels。spa
Goroutines 能够看做是轻量级线程。建立一个 goroutine 很是简单,只须要把 go 关键字放在函数调用语句前。为了说明这有多么简单,咱们建立两个 finder 函数,并用 go 调用,让它们每次找到 "ore" 就打印出来。线程
func main() { theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} go finder1(theMine) go finder2(theMine) <-time.After(time.Second * 5) //you can ignore this for now }
程序的输出以下:设计
Finder 1 found ore! Finder 2 found ore! Finder 1 found ore! Finder 1 found ore! Finder 2 found ore! Finder 2 found ore!
能够看出,两个 finder 是并发运行的。哪个先找到矿石没有肯定的顺序,当执行屡次程序时,这个顺序并不老是相同的。
这是一个很大的进步!如今咱们有一个简单的方法来建立多线程 (multi-gopher) 程序,可是当咱们须要独立的 goroutines 之间彼此通讯会发生什么呢?欢迎来到神奇的 channels 世界。
Channels 容许 go routines 之间相互通讯。你能够把 channel 看做管道,goroutines 能够往里面发消息,也能够从中接收其它 go routines 的消息。
myFirstChannel := make(chan string)
Goroutines 能够往 channel 发送消息,也能够从中接收消息。这是经过箭头操做符 (<-) 完成的,它指示 channel 中的数据流向。
myFirstChannel <-"hello" // Send myVariable := <- myFirstChannel // Receive
如今经过 channel 咱们可让寻矿 gopher 一找到矿石就当即传送给开矿 gopher ,而不用等发现全部矿石。
我重写了挖矿程序,把寻矿和开矿函数改写成了未命名函数。若是你从未见过 lambda 函数,没必要过多关注这部分,只须要知道每一个函数将经过 go 关键字调用并运行在各自的 goroutine 中。重要的是,要注意 goroutine 之间是如何经过 channel oreChan
传递数据的。别担忧,我会在最后面解释未命名函数的。
func main() { theMine := [5]string{"ore1", "ore2", "ore3"} oreChan := make(chan string) // Finder go func(mine [5]string) { for _, item := range mine { oreChan <- item //send } }(theMine) // Ore Breaker go func() { for i := 0; i < 3; i++ { foundOre := <-oreChan //receive fmt.Println("Miner: Received " + foundOre + " from finder") } }() <-time.After(time.Second * 5) // Again, ignore this for now }
从下面的输出,能够看到 Miner 从 oreChan
读取了三次,每次接收一块矿石。
Miner: Received ore1 from finder Miner: Received ore2 from finder Miner: Received ore3 from finder
太棒了,如今咱们能在程序的 goroutines(gophers) 之间发送数据了。在开始用 channels 写复杂的程序以前,咱们先来理解它的一些关键特性。
Channels 阻塞 goroutines 发生在各类情形下。这能在 goroutines 各自欢快地运行以前,实现彼此之间的短暂同步。
一旦一个 goroutine(gopher) 向一个 channel 发送数据,它就被阻塞了,直到另外一个 goroutine 从该 channel 取走数据。
和发送时情形相似,一个 goroutine 可能阻塞着等待从一个 channel 获取数据,若是尚未其余 goroutine 往该 channel 发送数据。
一开始接触阻塞的概念可能使人有些困惑,但你能够把它想象成两个 goroutines(gophers) 之间的交易。 其中一个 gopher 不管是等着收钱仍是送钱,都须要等待交易的另外一方出现。
既然已经了解 goroutine 经过 channel 通讯可能发生阻塞的不一样情形,让咱们讨论两种不一样类型的 channels: unbuffered 和 buffered 。选择使用哪种 channel 可能会改变程序的运行表现。
在前面的例子中咱们一直在用 unbuffered channels,它们不同凡响的地方在于每次只有一份数据能够经过。
在并发程序中,时间协调并不老是完美的。在挖矿的例子中,咱们可能遇到这样的情形:开矿 gopher 处理一块矿石所花的时间,寻矿 gohper 可能已经找到 3 块矿石了。为了避免让寻矿 gopher 浪费大量时间等着给开矿 gopher 传送矿石,咱们可使用 buffered channel。咱们先建立一个容量为 3 的 buffered channel。
bufferedChan := make(chan string, 3)
buffered 和 unbuffered channels 工做原理相似,但有一点不一样—在须要另外一个 gorountine 取走数据以前,咱们能够向 buffered channel 发送多份数据。
bufferedChan := make(chan string, 3) go func() { bufferedChan <-"first" fmt.Println("Sent 1st") bufferedChan <-"second" fmt.Println("Sent 2nd") bufferedChan <-"third" fmt.Println("Sent 3rd") }() <-time.After(time.Second * 1) go func() { firstRead := <- bufferedChan fmt.Println("Receiving..") fmt.Println(firstRead) secondRead := <- bufferedChan fmt.Println(secondRead) thirdRead := <- bufferedChan fmt.Println(thirdRead) }()
两个 goroutines 之间的打印顺序以下:
Sent 1st Sent 2nd Sent 3rd Receiving.. first second third
为了简单起见,咱们在最终的程序中不使用 buffered channels。但知道该使用哪一种 channel 是很重要的。
注意: 使用 buffered channels 并不会避免阻塞发生。例如,若是寻矿 gopher 比开矿 gopher 执行速度快 10 倍,而且它们经过一个容量为 2 的 buffered channel 进行通讯,那么寻矿 gopher 仍会发生屡次阻塞。
如今凭借 goroutines 和 channels 的强大功能,咱们可使用 Go 的并发原语编写一个充分发挥多线程优点的程序了。
theMine := [5]string{"rock", "ore", "ore", "rock", "ore"} oreChannel := make(chan string) minedOreChan := make(chan string) // Finder go func(mine [5]string) { for _, item := range mine { if item == "ore" { oreChannel <- item //send item on oreChannel } } }(theMine) // Ore Breaker go func() { for i := 0; i < 3; i++ { foundOre := <-oreChannel //read from oreChannel fmt.Println("From Finder:", foundOre) minedOreChan <-"minedOre" //send to minedOreChan } }() // Smelter go func() { for i := 0; i < 3; i++ { minedOre := <-minedOreChan //read from minedOreChan fmt.Println("From Miner:", minedOre) fmt.Println("From Smelter: Ore is smelted") } }() <-time.After(time.Second * 5) // Again, you can ignore this
程序输出以下:
From Finder: ore From Finder: ore From Miner: minedOre From Smelter: Ore is smelted From Miner: minedOre From Smelter: Ore is smelted From Finder: ore From Miner: minedOre From Smelter: Ore is smelted
相比最初的例子,已经有了很大改进!如今每一个函数都独立地运行在各自的 goroutines 中。此外,每次处理完一块矿石,它就会被带进挖矿流水线的下一个阶段。
为了专一于理解 goroutines 和 channel 的基本概念,上文有些重要的信息我没有提,若是不知道的话,当你开始编程时它们可能会形成一些麻烦。既然你已经理解了 goroutines 和 channel 的工做原理,在开始用它们编写代码以前,让咱们先了解一些你应该知道的其余信息。
相似于如何利用 go 关键字使一个函数运行在本身的 goroutine 中,咱们能够用以下方式建立一个匿名函数并运行在它的 goroutine 中:
// Anonymous go routine go func() { fmt.Println("I'm running in my own go routine") }()
若是只须要调用一次函数,经过这种方式咱们可让它在本身的 goroutine 中运行,而不须要建立一个正式的函数声明。
main 函数确实运行在本身的 goroutine 中!更重要的是要知道,一旦 main 函数返回,它将关掉当前正在运行的其余 goroutines。这就是为何咱们在 main 函数的最后设置了一个定时器—它建立了一个 channel,并在 5 秒后发送一个值。
<-time.After(time.Second * 5) // Receiving from channel after 5 sec
还记得 goroutine 从 channel 中读数据如何被阻塞直到有数据发送到里面吧?经过添加上面这行代码,main routine 将会发生这种状况。它会阻塞,以给其余 goroutines 5 秒的时间来运行。
如今有更好的方式阻塞 main 函数直到其余全部 goroutines 都运行完。一般的作法是建立一个 done channel, main 函数在等待读取它时被阻塞。一旦完成工做,向这个 channel 发送数据,程序就会结束了。
func main() { doneChan := make(chan string) go func() { // Do some work… doneChan <- "I'm all done!" }() <-doneChan // block until go routine signals work is done }
在前面的例子中咱们让 miner 在 for 循环中迭代 3 次从 channel 中读取数据。若是咱们不能确切知道将从 finder 接收多少块矿石呢?
好吧,相似于对集合数据类型 (注: 如 slice) 进行遍历,你也能够遍历一个 channel。
更新前面的 miner 函数,咱们能够这样写:
// Ore Breaker go func() { for foundOre := range oreChan { fmt.Println("Miner: Received " + foundOre + " from finder") } }()
因为 miner 须要读取 finder 发送给它的全部数据,遍历 channel 能确保咱们接收到已经发送的全部数据。
遍历 channel 会阻塞,直到有新数据被发送到 channel。在全部数据发送完以后避免 go routine 阻塞的惟一方法就是用 "close(channel)" 关掉 channel。
但你刚刚告诉咱们 channel 如何阻塞 goroutine 的各类情形?!没错,不过还有一个技巧,利用 Go 的 select case 语句能够实现对 channel 的非阻塞读。经过使用这这种语句,若是 channel 有数据,goroutine 将会从中读取,不然就执行默认的分支。
myChan := make(chan string) go func(){ myChan <- "Message!" }() select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println("No Msg") } <-time.After(time.Second * 1) select { case msg := <- myChan: fmt.Println(msg) default: fmt.Println("No Msg") }
程序输出以下:
No Msg Message!
非阻塞写也是使用一样的 select case 语句来实现,惟一不一样的地方在于,case 语句看起来像是发送而不是接收。
select { case myChan <- "message": fmt.Println("sent the message") default: fmt.Println("no message sent") }