package main
import (
"fmt"
"sync")
func main() {
var person sync.Map
// 将键值对保存在sync.Map
person.Store("张三", 26)
person.Store("李四", 30)
person.Store("王五", 32)
person.Store("赵六", 37)
// 从sync.Map中根据键取值
fmt.Println(person.Load("张三"))
// 根据键删除对应的键值对
person.Delete("张三")
// 遍历所有的sync.Map中的键值对
person.Range(func(key, value interface{}) bool {
fmt.Println("iterate: ", key,value)
return true
})
}
一个协程启动后,大部分情况需要等待里面的代码执行完毕,然后协程会自行退出。但是如果有一种情景,需要让协程提前退出怎么办呢?
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
watchDog("【监控狗1】")
}()
wg.Wait()
}
func watchDog(name string) {
// 开启for select循环,一直后台监控
for {
select {
default:
fmt.Println(name, "正在监控... ...")
}
time.Sleep(1 * time.Second)
}
}
通过 watchDog 函数实现了一个监控狗,它会一直在后台运行,每隔一秒就会打印"监控狗正在监控……"的文字。
如果需要让监控狗停止监控、退出程序,一个办法是定义一个全局变量,其他地方可以通过修改这个变量发出停止监控狗的通知。然后在协程中先检查这个变量,如果发现被通知关闭就停止监控,退出当前协程。
但是这种方法需要通过加锁来保证多协程下并发的安全,基于这个思路,有个升级版的方案:用 select+channel 做检测
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
// 用来停止监控狗
stopCh := make(chan bool)
go func() {
defer wg.Done()
watchDog(stopCh, "【监控狗1】")
}()
// 想让监控狗监控5秒
time.Sleep(5 * time.Second)
// 发出停止指令
stopCh <- true
wg.Wait()
}
func watchDog(stopCh chan bool,name string) {
// 开启for select循环,一直后台监控
for {
select {
case <- stopCh:
fmt.Println(name, "停止指令已收到,马上停止")
return
default:
fmt.Println(name, "正在监控... ...")
}
time.Sleep(1 * time.Second)
}
}
这个示例是使用 select+channel 的方式改造的 watchDog 函数,实现了通过 channel 发送指令让监控狗停止,进而达到协程退出的目的。
通过 select+channel 让协程退出的方式比较优雅,但是如果我们希望做到同时取消很多个协程呢?如果是定时取消协程又该怎么办?这时候 select+channel 的局限性就凸现出来了,即使定义了多个 channel 解决问题,代码逻辑也会非常复杂、难以维护。
要解决这种复杂的协程问题,必须有一种可以跟踪协程的方案,只有跟踪到每个协程,才能更好地控制它们,这种方案就是 Go 语言标准库为我们提供的 Context,也是这节课的主角。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
ctx, stop := context.WithCancel(context.Background())
go func() {
defer wg.Done()
watchDog(ctx, "【监控狗1】")
}()
// 先让监控狗监控5秒
time.Sleep(5 * time.Second)
// 发停止指令
stop()
wg.Wait()
}
func watchDog(ctx context.Context, name string) {
// 开启for select循环,一直后台监控
for {
select {
case <-ctx.Done():
fmt.Println(name, "停止指令已收到,马上停止")
return
default:
fmt.Println(name, "正在监控... ...")
}
time.Sleep(1 * time.Second)
}
}
可以看到,这和修改前的整体代码结构一样,只不过从 channel 换成了 Context。
一个任务会有很多个协程协作完成,一次 HTTP 请求也会触发很多个协程的启动,而这些协程有可能会启动更多的子协程,并且无法预知有多少层协程、每一层有多少个协程
如果因为某些原因导致任务终止了,HTTP 请求取消了,那么它们启动的协程怎么办?该如何取消呢?因为取消这些协程可以节约内存,提升性能,同时避免不可预料的 Bug。
Context 就是用来简化解决这些问题的,并且是并发安全的。Context 是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个协程之间的协作,尤其是取消操作。一旦取消指令下达,那么被 Context 跟踪的这些协程都会收到取消信号,就可以做清理和退出操作。
Context 接口只有四个方法,下面进行详细介绍,在开发中你会经常使用它们,你可以结合下面的代码来看。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Context 接口的四个方法中最常用的就是 Done 方法,它返回一个只读的 channel,用于接收取消信号。当 Context 取消的时候,会关闭这个只读 channel,也就等于发出了取消信号。
我们不需要自己实现 Context 接口,Go 语言提供了函数可以帮助我们生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 才可以关联起来,父 Context 发出取消信号的时候,子 Context 也会发出,这样就可以控制不同层级的协程退出。
从下图 Context 的衍生树可以看到,最顶部的是空 Context,它作为整棵 Context 树的根节点,在 Go 语言中,可以通过 context.Background() 获取一个根节点 Context。
有了根节点 Context 后,这颗 Context 树要怎么生成呢?需要使用 Go 语言提供的四个函数。
以上四个生成 Context 的函数中,前三个都属于可取消的 Context,它们是一类函数,最后一个是值 Context,用于存储一个 key-value 键值对。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(3)
ctx, stop := context.WithCancel(context.Background())
go func() {
defer wg.Done()
watchDog(ctx, "【监控狗1】")
}()
go func() {
defer wg.Done()
watchDog(ctx, "【监控狗2】")
}()
go func() {
defer wg.Done()
watchDog(ctx, "【监控狗3】")
}()
time.Sleep(5 * time.Second)
stop()
wg.Wait()
}
func watchDog(ctx context.Context, name string) {
// 开启for select循环,一直后台监控
for {
select {
case <-ctx.Done():
fmt.Println(name, "停止指令已收到,马上停止")
return
default:
fmt.Println(name, "正在监控... ...")
}
time.Sleep(1 * time.Second)
}
}
示例中增加了两个监控狗,也就是增加了两个协程,这样一个 Context 就同时控制了三个协程,一旦 Context 发出取消信号,这三个协程都会取消退出。
以上示例中的 Context 没有子 Context,如果一个 Context 有子 Context,在该 Context 取消时会发生什么呢?下面通过一幅图说明:
可以看到,当节点 Ctx2 取消时,它的子节点 Ctx4、Ctx5 都会被取消,如果还有子节点的子节点,也会被取消。也就是说根节点为 Ctx2 的所有节点都会被取消,其他节点如 Ctx1、Ctx3 和 Ctx6 则不会。
Context 不仅可以取消,还可以传值,通过这个能力,可以把 Context 存储的值供其他协程使用。
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(4)
ctx, stop := context.WithCancel(context.Background())
valCtx := context.WithValue(ctx, "userId", 2)
go func() {
defer wg.Done()
getUser(valCtx)
}()
go func() {
defer wg.Done()
watchDog(ctx, "【监控狗1】")
}()
go func() {
defer wg.Done()
watchDog(ctx, "【监控狗2】")
}()
go func() {
defer wg.Done()
watchDog(ctx, "【监控狗3】")
}()
time.Sleep(5 * time.Second)
stop()
wg.Wait()
}
func getUser(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("【获取用户】", "协程退出")
return
default:
userId := ctx.Value("userId")
fmt.Println("【获取用户】", "用户ID为", userId)
time.Sleep(1 * time.Second)
}
}
}
func watchDog(ctx context.Context, name string) {
// 开启for select循环,一直后台监控
for {
select {
case <-ctx.Done():
fmt.Println(name, "停止指令已收到,马上停止")
return
default:
fmt.Println(name, "正在监控... ...")
}
time.Sleep(1 * time.Second)
}
}
通过 context.WithValue 函数存储一个 userId 为 2 的键值对,就可以在 getUser 函数中通过 ctx.Value("userId") 方法把对应的值取出来,达到传值的目的。
Context 是一种非常好的工具,使用它可以很方便地控制取消多个协程。在 Go 语言标准库中也使用了它们,比如 net/http 中使用 Context 取消网络的请求。
要更好地使用 Context,有一些使用原则需要尽可能地遵守。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。