这是我参与「第五届青训营 」伴学笔记创作活动的第 9 天
1.复习已学知识
- 并发和并行的区别
- 学习并发的概念
- 使用Goroutine
2.观看Go语言进阶与依赖管理
- 学习WaitGroup
看看如下的例子:
func hello(i int) {
println("hello goroutine : " + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
func main() {
HelloGoRoutine()
}
一个简单的协程程序,其中 HelloGoRoutine 函数开启了 5 个 go 协程,用于打印输出 1-5 的数字,最后,对主协程进行休眠以确保子协程执行完成。
如果您对这段代码有所疑惑,那么对于:
func(j int) {
hello(j)
}
这部分代码,其实是 Go 的匿名函数语法,他创建了一个未定义名称的函数,声明了一个 int 类型的形参 j,并在该函数的函数体内调用了 hello 函数。接下来在其后面添加()并传入实参 i 以直接调用这个匿名函数。
为了保证子协程执行完毕后主协程才会退出,改造 HelloGoRoutine 函数:
func HelloGoRoutine() {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
time.Sleep(time.Second) 被移除,取而代之的是 wg.Wait(),除此之外,函数还增加了许多其他调用,它们都是什么意思?让我们一步一步解析:
- 首先,
var wg sync.WaitGroup声明了一个名为wg的WaitGroup变量; - 紧接着,调用
WaitGroup实例的Add方法,传入数字5; - 在匿名函数开头,延迟调用
WaitGroup实例的Done方法; - 最后,在函数尾部调用
WaitGroup实例的Wait方法。
如果您还不了解 defer 关键字,其实该关键字代表“在函数末尾执行”,被 defer 关键字标注的代码会以 先进后出 的顺序被移动到函数末尾执行,这就意味着:
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
实际上等价于:
fmt.Println("3")
fmt.Println("2")
fmt.Println("1")
defer 关键字常用于资源释放等用处:比如,我们打开了一个 IO 流,需要在函数结束时释放流(调用 Close 方法),这时,我们便可以直接在打开的流实例下直接填写:
defer stream.Close()
来确保流一定会在函数结束时释放,避免遗忘。
回到开头,WaitGroup 其实内部维护了一个计数器,并以如下方式工作:
- 通过调用
Add方法,向WaitGroup的计数器添加指定值; - 通过调用
Wait方法阻塞当前协程,这会使得协程陷入无限的等待; - 通过调用
Done方法使WaitGroup内部的计数器 -1,直到计数器值为 0 时,先前被阻塞的协程便会被释放,继续执行接下来的代码或是直接结束运行。
因此,上述代码为 WaitGroup 的计数器 +5,随后阻塞主协程,当所有 5 个子协程纷纷调用 Done 方法后,主协程便会被释放,然后结束程序运行。怎么样,是不是很优雅?