相信大家踏入Go
语言的世界,肯定是被强大的并发(Concurrency
)所吸引,Go
语言用最简单的关键字go
就可以将任务丢到后台处理,但是开发者怎么有效率的控制并发,这是入门Go
语言必学的技能,本章会介绍几种方式来带大家认识并发,而这三种方式分别对应到三个不同的名词:WaitGroup
,Channel
,及 Context
。下面用简单的范例带大家了解。
WaitGroup
先来了解有什么情境需要使用到 WaitGroup
,假设您有两台机器需要同时上传最新的代码,两台机器分别上传完成后,才能执行最后的重启步骤。就像是把一个工作同时拆成好几份同时一起做,可以减少时间,但是最后需要等到全部做完,才能执行下一步,这时候就需要用到 WaitGroup
才能做到。
(推荐课程:Go教程)
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
i := 0
wg.Add(3) //task count wait to do
go func() {
defer wg.Done() // finish task1
fmt.Println("goroutine 1 done")
i++
}()
go func() {
defer wg.Done() // finish task2
fmt.Println("goroutine 2 done")
i++
}()
go func() {
defer wg.Done() // finish task3
fmt.Println("goroutine 3 done")
i++
}()
wg.Wait() // wait for tasks to be done
fmt.Println("all goroutine done")
fmt.Println(i)
}
Channel
另外一种实际的案例就是,我们需要主动通知一个 Goroutine
进行停止的动作。换句话说,当 App 启动时,会在后台跑一些监控程序,而当整个 App 需要停止前,需要发个 Notification
给后台的监控程序,将其先停止,这时候就需要用到 Channel
来通知。看下下面这个例子:
package main
import (
"fmt"
"time"
)
func main() {
exit := make(chan bool)
go func() {
for {
select {
case <-exit:
fmt.Println("Exit")
return
case <-time.After(2 * time.Second):
fmt.Println("Monitoring")
}
}
}()
time.Sleep(5 * time.Second)
fmt.Println("Notify Exit")
exit <- true //keep main goroutine alive
time.Sleep(5 * time.Second)
}
上面的例子可以发现,用了一个 Gogourtine
和 Channel
来控制。可以想像当后台有无数个 Goroutine
的时候,我们就需要用多个 Channel
才能进行控制,也许 Goroutine
内又会产生 Goroutine
,开发者这时候就会发现已经无法单纯使用 Channel
来控制多个 Goroutine
了。这时候解决方式会是传递 Context
。
(推荐课程:Go Web编程)
Context
大家可以想像,今天有一个后台任务 A,A 任务又产生了 B 任务,B 任务又产生了 C 任务,也就是可以按照此模式一直产生下去,假设中途我们需要停止 A 任务,而 A 又必须告诉 B 及 C 要一起停止,这时候通过 context
方式是最快的了。
package main
import (
"context"
"fmt"
"time"
)
func foo(ctx context.Context, name string) {
go bar(ctx, name) // A calls B
for {
select {
case <-ctx.Done():
fmt.Println(name, "A Exit")
return
case <-time.After(1 * time.Second):
fmt.Println(name, "A do something")
}
}
}
func bar(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "B Exit")
return
case <-time.After(2 * time.Second):
fmt.Println(name, "B do something")
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go foo(ctx, "FooBar")
fmt.Println("client release connection, need to notify A, B exit")
time.Sleep(5 * time.Second)
cancel() //mock client exit, and pass the signal, ctx.Done() gets the signal time.Sleep(3 * time.Second)
time.Sleep(3 * time.Second)
}
package main
import (
"context"
"fmt"
"time"
)
func foo(ctx context.Context, name string) {
go bar(ctx, name) // A calls B
for {
select {
case <-ctx.Done():
fmt.Println(name, "A Exit")
return
case <-time.After(1 * time.Second):
fmt.Println(name, "A do something")
}
}
}
func bar(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "B Exit")
return
case <-time.After(2 * time.Second):
fmt.Println(name, "B do something")
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go foo(ctx, "FooBar")
fmt.Println("client release connection, need to notify A, B exit")
time.Sleep(5 * time.Second)
cancel() //mock client exit, and pass the signal, ctx.Done() gets the signal time.Sleep(3 * time.Second)
time.Sleep(3 * time.Second)
}
大家可以把 context
想成是一个 controller
,可以随时控制不确定个数的 Goroutine
,由上往下,只要宣告context.WithCancel
后,再任意时间点都可以通过cancel()
来停止整个后台服务。实际案例会用在当 App 需要重新启动时,要先通知全部 goroutine
停止,正常停止后,才会重新启动 App。
(推荐微课:Go微课)
总结
根据不同的情境跟状况来选择不同的方式,做一个总结:
- WaitGroup:需要将单一个工作分解成多个子任务,等到全部完成后,才能进行下一步,这时候用
WaitGroup
最适合了 - Channel + Select:
Channel
只能用在比较单纯的Goroutine
情况下,如果要管理多个Goroutine
,建议还是 走context
会比较适合 - Context:如果您想一次控制全部的
Goroutine
,相信用context
会是最适合不过的,当然context
不只有这特性,详细可以参考『用 10 分钟了解 Go 语言 context package 使用场景及介绍』
以上就是关于Go 语言中管理 Concurrency
的三种方式的相关介绍了,希望对大家有所帮助。