引言
Go语言的一个流行特性是它对并发的一流支持,即一个程序可以同时做多件事。随着计算机从更快地运行单个代码流转向同时运行更多代码流,能够并发地运行代码正在成为编程的重要组成部分。为了让程序运行得更快,程序员需要把程序设计成并发运行,这样程序中并发的每一部分都可以独立于其他部分运行。Go中的两个特性,goroutines和channels,在一起使用时使并发更容易。协程解决了在程序中设置和运行并发代码的困难,通道解决了并发运行的代码之间安全通信的困难。
在本教程中,你将探索goroutines和channels。首先,你将创建一个使用goroutines一次运行多个函数的程序。然后,您将向该程序添加通道,以便在运行的goroutines之间进行通信。最后,你将向程序中添加更多的goroutines,以模拟程序在多个worker goroutines下运行。
与Goroutines同时运行函数
在现代计算机中,处理器(CPU)被设计成同时运行尽可能多的代码流。这些处理器有一个或多个“核心”,每个核心能够同时运行一个代码流。因此,程序可以同时使用的内核越多,程序运行得就越快。然而,为了让程序利用多核带来的速度提升,程序需要能够拆分为多个代码流。将程序拆分为多个部分可能是编程中最具挑战性的事情之一,但Go的设计使这一过程更容易。
一种方法是使用** goroutines 功能。goroutine是一种特殊类型的函数,它可以在其他goroutine运行时运行。当一个程序被设计为一次运行多个代码流时,该程序被设计为并发**运行。通常,当函数被调用时,它将在代码继续运行之前完全运行完毕。这被称为在“前台”运行,因为它阻止程序在完成之前做任何其他事情。使用goroutine,当goroutine在“后台”运行时,函数调用将继续立即运行下一段代码。当代码在完成之前不阻止其他代码运行时,就认为它在后台运行。
goroutines提供的功能是每个goroutine可以同时在一个处理器内核上运行。如果您的计算机有四个处理器内核,并且您的程序有四个goroutines,所有四个goroutines可以同时运行。当多个代码流同时在不同的内核上运行时,我们称之为并行运行。
为了可视化并发和并行之间的区别,请考虑下面的图。当处理器运行一个函数时,它并不总是从头到尾一次性运行所有函数。有时,当一个函数在等待其他事情发生(例如读取文件)时,操作系统会交错调用其他函数、协程或CPU内核上的其他程序。该图显示了一个为并发设计的程序如何可以在单核上运行,也可以在多核上运行。它还显示了并行运行时,与在单核上运行时相比,goroutine的更多段可以装入相同的时间帧(9个垂直段,如图所示)。
图中的左列,标记为“并发”,展示了围绕并发设计的程序如何通过运行goroutine1
的一部分,然后运行另一个函数、goroutine或程序,然后运行goroutine2
,然后再次运行goroutine1
,以此在单个CPU内核上运行。对于用户来说,这看起来像是程序在同时运行所有函数或协程,即使它们实际上是一个接一个地在小的部分中运行。
图中右侧标记为“Parallelism”的列展示了同一个程序如何在具有两个CPU内核的处理器上并行运行。第一个CPU核心显示goroutine1
与其他函数、goroutines或程序一起运行,而第二个CPU核心显示goroutine2
与该核心上的其他函数或goroutines一起运行。有时goroutine1
和goroutine2
同时运行,只是在不同的CPU内核上。
此图还展示了Go的另一个强大特性,可扩展性。当一个程序可以运行在任何设备上(从只有几个处理器内核的小型计算机到拥有几十个内核的大型服务器),并利用这些额外的资源时,它就是可扩展的。该图显示,通过使用goroutines,你的并发程序能够在单个CPU核上运行,但随着更多的CPU核的添加,更多的goroutines可以并行运行以加速程序。
要开始使用新的并发程序,请在你选择的位置创建一个multifunc
目录。你可能已经为你的项目创建了一个目录,但在本教程中,你将创建一个名为projects
的目录。你可以通过IDE或命令行创建projects
目录。
如果你使用的是命令行,首先创建projects
目录并导航到它:
mkdir projects
cd projects
在projects
目录中,使用mkdir
命令创建程序的目录(multifunc
),然后导航到它:
mkdir multifunc
cd multifunc
进入multifunc
目录后,使用nano
或你最喜欢的编辑器打开一个名为main.go
的文件:
nano main.go
在main.go
文件中粘贴或输入以下代码以开始。
projects/multifunc/main.go
package main
import (
"fmt"
)
func generateNumbers(total int) {
for idx := 1; idx <= total; idx++ {
fmt.Printf("Generating number %d\n", idx)
}
}
func printNumbers() {
for idx := 1; idx <= 3; idx++ {
fmt.Printf("Printing number %d\n", idx)
}
}
func main() {
printNumbers()
generateNumbers(3)
}
这个初始程序定义了两个函数,generateNumbers
和printNumbers
,然后在main
函数中运行这些函数。generateNumbers
函数以要“generate”的数字的数量作为参数,在本例中是1到3,然后将这些数字都打印到屏幕上。printNumbers
函数还没有接受任何参数,但它也会打印出1到3的数字。
保存main.go
文件后,使用go run
运行它以查看输出:
go run main.go
OutputPrinting number 1
Printing number 2
Printing number 3
Generating number 1
Generating number 2
Generating number 3
你会看到这些函数一个接一个地运行,printNumbers
首先运行,generateNumbers
其次运行。
现在,想象一下printNumbers
和generateNumbers
的运行时间都是三秒。当同步运行时,或者像上一个例子那样一个接一个地运行时,你的程序需要6秒才能运行。首先,printNumbers
将运行三秒,然后generateNumbers
将运行三秒。然而,在你的程序中,这两个函数是相互独立的,因为它们运行时不依赖于对方的数据。你可以利用这一点,通过使用goroutines并发运行函数来加速这个假设的程序。理论上,当两个函数同时运行时,程序的运行时间可以减少一半。如果printNumbers
和generateNumbers
函数运行都需要3秒,并且都在同一时间开始,程序可能在3秒内完成。(不过,实