并发控制
概述
参考:
context 是在Go 语言1.7 版才正式被纳入官方标准库内,为什么今天要介绍 context 使用方式呢?原因很简单,在初学 Go 时,写 API 时,常常不时就会看到在 http handler 的第一个参数就会是ctx context.Context
,而这个 context 在这边使用的目的及含义到底是什么呢,本篇就是带大家了解什么是 context,以及使用的场景及方式,内容不会提到 context 的原始码,而是用几个实际例子来了解。
如果对于课程内容有兴趣,可以参考底下课程。
- Go 语言基础实战(开发, 测试及部署)
- 一天学会 DevOps 自动化测试及部署
- DOCKER 容器开发部署实战(课程刚启动,限时特价$900 TWD)
如果需要搭配购买请直接透过FB 联络我,直接汇款(价格再减100)
使用 WaitGroup
学 Go 时肯定要学习如何使用并发(goroutine),而开发者该如何控制并发呢?其实有两种方式,一种是WaitGroup,另一种就是 context,而什么时候需要用到 WaitGroup 呢?很简单,就是当我们需要将同一件事情拆成不同的 Job 下去执行,最后需要等到全部的 Job 都执行完毕才继续执行主程式,这时候就需要用到 WaitGroup,看个实际例子
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
time.Sleep(2 * time.Second)
fmt.Println("job 1 done.")
wg.Done()
}()
go func() {
time.Sleep(1 * time.Second)
fmt.Println("job 2 done.")
wg.Done()
}()
wg.Wait()
fmt.Println("All Done.")
}
上面范例可以看到主程式透过 wg.Wait()
来等待全部 job 都执行完毕,才印出最后的讯息。这边会遇到一个情境就是,虽然把 job 拆成多个,并且丢到背景去跑,可是使用者该如何透过其他方式来终止相关 goroutine 工作呢(像是开发者都会写背景程式监控,需要长时间执行)?例如 UI 上面有停止的按钮,点下去后,如何主动通知并且停止正在跑的 Job,这边很简单,可以使用 channel + select 方式。
使用 channel + select
package main
import (
"fmt"
"time"
)
func main() {
stop := make(chan bool)
go func() {
for {
select {
case <-stop:
fmt.Println("got the stop channel")
return
default:
fmt.Println("still working")
time.Sleep(1 * time.Second)
}
}
}()
time.Sleep(5 * time.Second)
fmt.Println("stop the gorutine")
stop <- true
time.Sleep(5 * time.Second)
}
上面可以看到,透过 select + channel 可以快速解决这问题,只要在任何地方将 bool 值丢入 stop channel 就可以停止背景正在处理的 Job。上述用 channel 来解决此问题,但是现在有个问题,假设背景有跑了无数个 goroutine,或者是 goroutine 内又有跑 goroutine 呢,变得相当复杂,例如底下的状况
这边就没办法用 channel 方式来进行处理了,而需要用到 context。
认识 context
从上图可以看到我们建立了三个 worker node 来处理不同的 Job,所以会在主程式最上面宣告一个主context.Background()
,然后在每个 worker node 分别在个别建立子 context,其最主要目的就是当关闭其中一个 context 就可以直接取消该 worker 内正在跑的 Job。拿上面的例子进行改写
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("got the stop channel")
return
default:
fmt.Println("still working")
time.Sleep(1 * time.Second)
}
}
}()
time.Sleep(5 * time.Second)
fmt.Println("stop the gorutine")
cancel()
time.Sleep(5 * time.Second)
}
其实可以看到只是把原本的 channel 换成使用 context 来处理,其他完全不变,这边提到使用了context.WithCancel
,使用底下方式可以扩充 context
ctx, cancel := context.WithCancel(context.Background())
这用意在于每个 worknode 都有独立的 cancel func
开发者可以透过其他地方呼叫 cancel() 来决定哪一个 worker 需要被停止,这时候可以做到使用 context 来停止多个 goroutine 的效果,底下看看实际例子
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, "node01")
go worker(ctx, "node02")
go worker(ctx, "node03")
time.Sleep(5 * time.Second)
fmt.Println("stop the gorutine")
cancel()
time.Sleep(5 * time.Second)
}
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "got the stop channel")
return
default:
fmt.Println(name, "still working")
time.Sleep(1 * time.Second)
}
}
}
上面透过一个 context 可以一次停止多个 worker,看逻辑如何宣告 context 以及什么时机去执行 cancel(),通常我个人都是搭配 graceful shutdown 进行取消正在跑的 Job,或者是停止资料库连线等等..
心得
初学 Go 时,如果还不常使用 goroutine,其实也不会理解到 context 的使用方式及时机,等到需要有背景处理,以及该如何停止 Job,这时候才渐渐了解到使用方式,当然 context 不只有这个使用方式,未来还会介绍其他使用方式。
See also
- 用 10 分钟了解 Go 语言如何从 Channel 读取资料
- 用 Go 语言实现 Pub-Sub 模式
- Go 语言实作 Graceful Shutdown 套件
- 使用 AWS IAM Policy 设定 S3 Bucket 底下特定目录权限
- 推荐自动化监控网站运行服务- Gatus
- 使用 pyroscope adhoc 加速找到效能瓶颈
- Go 1.18 generics 新套件 constraints, slices 及 maps
- 初探 golang 1.18 generics 功能
- Introduction to Open Policy Agent 初探权限控管机制
- 用 Supervisor 管理系统程式
反馈
此页是否对你有帮助?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.