Go 语言协程(goroutine)
协程(goroutine)是 Go 语言中一种轻量级的并发执行单元,由 Go 运行时环境(runtime)管理。协程可以理解为一种比线程更轻量级的并发模型,它可以并发执行函数或方法,但消耗的资源更少,并且可以更高效地使用系统资源。
本文所有示例代码可在 GitHub 下载。
创建 goroutine
Go 语言中使用 goroutine 非常简单,只需要在函数或者方法前面加上 go
关键字就可以创建一个 goroutine,从而让该函数或者方法在新的 goroutine 中执行。
例如:
go hello()
匿名函数同样也支持使用 go
关键字来创建 goroutine
去执行。语法格式如下:
go func() {
// 协程的执行逻辑
}()
注意:一个 goroutine 必定对应一个函数或者方法,你可以创建多个 goroutine 去执行相同的函数或者方法。
启动单个 goroutine
启动方式非常简单,我们先来看一个实例:
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello")
}
func main() {
go hello()
fmt.Println("欢迎来到 GetIoT.tech!")
}
以上代码输出结果如下:
欢迎来到 GetIoT.tech!
上述代码执行结果只在终端控制台输 出了“欢迎来到 GetIoT.tech!”,并没有打印“Hello”,这是为什么呢 ?
其实在 Go 程序中,会默认为 main 函数创建一个 goroutine,而在上述代码中我们使用 go 关键字创建了一个新的 goroutine 去调用 hello 函数。而此时 main 的 goroutine 还在往下执行中,我们的程序中存在两个并发执行的 goroutine。当 main 函数结束时,整个程序也结束了,所有由 main 函数创建的子 goroutine 也会跟着退出,也就是说我们的 main 函数执行过快退出导致另一个 goroutine 内容还未执行就退出了,导致未能打印出“Hello”。
所以我们这边要想办法让 main 函数等一等,让另一个 goroutine 的内容执行完。其中最简单的方法就是在 main 函数中使用 time.Sleep
睡眠一秒钟。
按如下方式修改
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello")
}
func main() {
go hello()
fmt.Println("欢迎来到 GetIoT.tech!")
time.Sleep(time.Second)
}
此时的输出结果为:
欢迎来到 GetIoT.tech!
Hello
那为什么会先打印“欢迎来到 GetIoT.tech!”,再打印“Hello”呢?
这是因为在程序中创建 goroutine 执行函数需要一定的开销,而与此同时 main 函数所在的 goroutine 是继续执行的。
使用 sync.WaitGroup
前面的实例虽然使用 time.sleep
可以让 Hello 打印出来,但这个方法是不准确的,因为你无法准确计算 goroutine 的运行时间。因此我们需要使用 Go 语言中的 sync
包,它为我们提供了一些常用的并发原语。
首先,我们介绍一下 sync
包中的 WaitGroup
。当你并不关心并发操作的结果或者有其他方式收集并发 操作的结果时,WaitGroup
是实现等待一组并发操作完成的好方法。
我们修改一下前面的示例,如下:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello() {
fmt.Println("Hello")
defer wg.Done() // 把计算器-1
}
func main() {
wg.Add(1) // 把计数器+1
go hello()
fmt.Println("欢迎来到 GetIoT.tech!")
wg.Wait() // 阻塞代码的运行,直到计算器为0
}
defer
语句用于延迟函数或方法的执行,使其在函数执行结束时执行。参见《Go 语言函数进阶》。
以上代码输出结果如下:
欢迎来到 GetIoT.tech!
Hello
启动多个 goroutine
接下来我们看看如何启动多个 goroutine。请看下面实例:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int) {
fmt.Printf("[%v] Hello, welcome to GetIoT.tech!\n", i)
defer wg.Done() // goroutine 结束计数器-1
}
func main() {
for i := 0; i < 5; i++ {
go hello(i)
wg.Add(1) // 启动一个goroutine计数器+1
}
wg.Wait() // 等待所有的goroutine执行结束
}
以上代码执行结果如下:
[4] Hello, welcome to GetIoT.tech!
[0] Hello, welcome to GetIoT.tech!
[1] Hello, welcome to GetIoT.tech!
[2] Hello, welcome to GetIoT.tech!
[3] Hello, welcome to GetIoT.tech!
执行多次上述代码你会发现输出顺序并不一致,这是因为多个 goroutine 都是并发执行的,而 goroutine 的调度是随机的。