了解 goroutine(轻量线程)
并发是独立活动的组合,就像 Web 服务器虽然同时处理多个用户请求,但它是自主运行的。 并发在当今的许多程序中都存在。 Web 服务器就是一个例子,但你也能看到,在批量处理大量数据时也需要使用并发。
Go 有两种编写并发程序的样式。 一种是在其他语言中通过线程实现的传统样式。 在本模块中,你将了解 Go 的样式,其中值是在称为 goroutine 的独立活动之间传递的,以与进程进行通信。
如果这是你第一次学习并发,我们建议你多花一些时间来查看我们将要编写的每一段代码,以进行实践。
Go 实现并发的方法
通常,编写并发程序时最大的问题是在进程之间共享数据。 Go 采用不同于其他编程语言的通信方式,因为 Go 是通过 channel 来回传递数据的。 此方法意味着只有一个活动 (goroutine) 有权访问数据,设计上不存在争用条件。 学完本模块中的 goroutine 和 channel 之后,你将更好地理解 Go 的并发方法。
Go 的方法可以总结为以下口号:“不是通过共享内存通信,而是通过通信共享内存。”我们将在以下部分介绍此方法,但你也可以在 Go 博客文章通过通信共享内存中了解详细信息。
如前所述,Go 还提供低级别的并发基元。 但在本模块中,我们只介绍 Go 的惯用并发方法。
让我们从探索 goroutine 开始。
Goroutine
goroutine 是轻量线程中的并发活动,而不是在操作系统中进行的传统活动。 假设你有一个写入输出的程序和另一个计算两个数字相加的函数。 一个并发程序可以有数个 goroutine 同时调用这两个函数。
我们可以说,程序执行的第一个 goroutine 是 main()
函数。 如果要创建其他 goroutine,则必须在调用该函数之前使用 go
关键字,如下所示:
func main(){
login()
go launch()
}
你还会发现,许多程序喜欢使用匿名函数来创建 goroutine,如此代码中所示:
func main(){
login()
go func() {
launch()
}()
}
为了查看运行中的 goroutine,让我们编写一个并发程序。
编写并发程序
由于我们只想将重点放在并发部分,因此我们使用现有程序来检查 API 终结点是否响应。 代码如下:
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
for _, api := range apis {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
continue
}
fmt.Printf("SUCCESS: %s is up and running!\n", api)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
运行前面的代码时,将看到以下输出:
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 1.658436834 seconds!
这里没有什么特别之处,但我们可以做得更好。 或许我们可以同时检查所有站点? 此程序可以在 500 毫秒的时间内完成,不需要耗费将近两秒。
请注意,我们需要并发运行的代码部分是向站点进行 HTTP 调用的部分。 换句话说,我们需要为程序要检查的每个 API 创建一个 goroutine。
为了创建 goroutine,我们需要在调用函数前使用 go
关键字。 但我们在这里没有函数。 让我们重构该代码并创建一个新函数,如下所示:
func checkAPI(api string) {
_, err := http.Get(api)
if err != nil {
fmt.Printf("ERROR: %s is down!\n", api)
return
}
fmt.Printf("SUCCESS: %s is up and running!\n", api)
}
注意,我们不再需要 continue
关键字,因为我们不在 for
循环中。 要停止函数的执行流,只需使用 return
关键字。 现在,我们需要修改 main()
函数中的代码,为每个 API 创建一个 goroutine,如下所示:
for _, api := range apis {
go checkAPI(api)
}
重新运行程序,看看发生了什么。
看起来程序不再检查 API 了,对吗? 显示的内容可能与以下输出类似:
Done! It took 1.506e-05 seconds!
速度可真快! 发生了什么情况? 你会看到最后一条消息,指出程序已完成,因为 Go 为循环中的每个站点创建了一个 goroutine,并且它立即转到下一行。
即使看起来 checkAPI
函数没有运行,它实际上是在运行。 它只是没有时间完成。 请注意,如果在循环之后添加一个睡眠计时器会发生什么,如下所示:
for _, api := range apis {
go checkAPI(api)
}
time.Sleep(3 * time.Second)
现在,重新运行程序时,可能会看到如下所示的输出:
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
Done! It took 3.002114573 seconds!
看起来似乎起作用了,对吧? 不完全如此。 如果你想在列表中添加一个新站点呢? 也许三秒钟是不够的。 你怎么知道? 你无法管理。 必须有更好的方法,这就是我们在下一节讨论 channel 时要涉及的内容。