一、sync.WaitGroup{} 进行goroutine的控制
WaitGroup等待组是控制goroutine并发的一种方式,在我看来真正的意义是照顾弱势的“子goroutine”,通过Add()方法,确定预期要运行的goroutine的个数,然后goroutine内部通过defer延迟函数执行Done()方法,对一个goroutine运行结束后,进行计数减一,最后在“父goroutine”中的某个位置通过Wait()方法等待所有“子goroutine”执行结束,才能继续“父goroutine”的其他工作。
示例代码如下:
package main
import (
"sync"
"fmt"
)
func main() {
var wg sync.WaitGroup
fmt.Println("Prepare get in goroutine")
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("I`m in a goroutine.")
}()
fmt.Println("out the goroutine.")
wg.Wait() // 等待main中的goroutine执行完,才能继续下去
fmt.Println("main is stopping")
}
二、简陋地给goroutine的函数传channel值或通过作用域的channel变量进行控制
通过给函数传递一个通道参数,再通过select的其中一个case进行该通道接收判定,如果收到值就会执行终止goroutine的操作。
通过作用域的通道变量是指,该通道变量不是在goroutine中声明的,换句话说是在该goroutine外面对声明的这个通道变量进行传值操作,会对该goroutine中这个通道变量的case判定造成接收,进而可以执行终止goroutine的操作。
下面是通过goroutine的函数参数传值进行控制的方式。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan struct{})
go printSomething(ch)
time.Sleep(5*time.Second)
close(ch)
time.Sleep(10*time.Second)
}
func printSomething(ch chan struct{}) {
ticker := time.NewTicker(3*time.Second)
for {
select {
case <-ch:
fmt.Println("finished printing work by channel")
return
case <-ticker.C:
fmt.Println("finished printing work by time.Ticker")
return
default:
time.Sleep(time.Second)
fmt.Println("Hello World.")
}
}
}
下面的示例是在一个作用域下通过通道传值影响到goroutine内的这个等待传值的通道来控制goroutine的方式。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan bool)
go func() {
for {
select {
case <-ch:
fmt.Println("stop the goroutine...")
return
default:
time.Sleep(time.Second)
fmt.Println("Doria`s sheep is eating grass.")
}
}
}()
time.Sleep(5*time.Second)
ch <- true
}
三、context进行goroutine的控制
context实现对goroutine的控制是通过该context的对象的取消函数对应的Done()函数,即一个struct{}类型的通道,当执行WithCancel()函数,会返回一个cancelFunc,当执行该函数时,会通知goroutine中的ctx.Done()函数,然后可以借此进行goroutine的停止退出。另外衍生出的方法还有WithDeadline(),WithTimeout(),WithValue(),其中WithDeadline(),WithTimeout()的功能大致与WithCancel()差不多,只是多了一个截止时间参数,而WithValue()是通过传入的key和val,在goroutine中传入实参context,然后共享到这个context的key的val值。
示例如下
package main
import (
"context"
"fmt"
"time"
)
var lastName string = "lastName"
func main() {
ctx, cancel := context.WithCancel(context.Background())
ctxHasValue := context.WithValue(ctx, lastName, "Stark")
go sayHello(ctxHasValue, "Snow")
go sayHello(ctxHasValue, "Joe")
time.Sleep(5*time.Second)
cancel()
time.Sleep(5*time.Second)
fmt.Println("main ends...")
}
func sayHello(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s says: Goodbye, Arya %s...\n", name, ctx.Value(lastName))
return
default:
time.Sleep(time.Second)
fmt.Printf("Hello, Arya %s, I`m %s\n", ctx.Value(lastName), name)
}
}
}
四、总结
简单总结一下,WaitGroup的作用是保证预期要执行的“子goroutine”都能执行到,而不被“父goroutine”欺负。而通过通道传值和context控制goroutine的停止,其实本质是没有太大区别的。但值得一提的是,前段时间在网上看过一篇文章,说Google公司的程序员写goroutine并发被要求并发函数的第一个参数需要是ctx context.Context
,不论真假,至少说明了一件事,使用到context来控制goroutine是非常美观和方便的。在高并发的开发场景下,goroutine之间的通信和控制会变得异常艰难,如果使用到context,可以安全地停止goroutine,方便地进行通信操作。