您诸位好啊,我是无尘,今天接着介绍下并发中的操作--Context。
一个协程启动后,一般是代码执行完毕,自动退出,但是如果需要提前终止怎么办呢? 一个办法是定义一个全局变量,协程中通过检查这个变量的变化来决定是否退出。这种办法须要加锁来保证并发安全,说到这里,有没有想的什么解决方案? 「select + channel」 来实现:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
stopWk := make(chan bool)
wg.Add(1)
go func() {
defer wg.Done()
worker(stopWk)
}()
time.Sleep(3*time.Second) //工作3秒
stopWk <- true //3秒后发出停止指令
wg.Wait()
}
func worker(stopWk chan bool){
for {
select {
case <- stopWk:
fmt.Println("下班咯~~~")
return
default:
fmt.Println("认真摸鱼中,请勿打扰...")
}
time.Sleep(1*time.Second)
}
}
运行结果:
认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
下班咯~~~
可以看到,每秒打印一次“认真摸鱼中,请勿打扰...”,3秒后发出停止指令,程序进入 “下班咯~~~”。
上面我们使用 select+channel 来实现了协程的终止,但是如果我们想要同时取消多个协程怎么办呢?如果需要定时取消又怎么办呢? 此时,Context 就需要登场了,它可以跟踪每个协程,我们重写上面的示例:
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ctx, stop := context.WithCancel(context.Background())
wg.Add(1)
go func() {
defer wg.Done()
worker(ctx)
}()
time.Sleep(3*time.Second) //工作3秒
stop() //3秒后发出停止指令
wg.Wait()
}
func worker(ctx context.Context){
for {
select {
case <- ctx.Done():
fmt.Println("下班咯~~~")
return
default:
fmt.Println("认真摸鱼中,请勿打扰...")
}
time.Sleep(1*time.Second)
}
}
运行结果:
认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
认真摸鱼中,请勿打扰...
下班咯~~~
Context 是并发安全的,它是一个接口,可以手动、定时、超时发出取消信号、传值等功能,主要是用于控制多个协程之间的协作、取消操作。
Context 接口有四个方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
❝最常用的是 Done 方法,在 Context 取消的时候,会关闭这个只读的 Channel,相当于发出了取消信号。 ❞
我们并不需要自己去实现 Context 接口,Go 语言提供了函数来生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 就可以关联起来,父级 Context 发出取消信号,子级 Context 也会发出,这样就可以控制不同层级的协程退出。
emptyCtx
是一个int类型的变量,但实现了context的接口。emptyCtx
没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx
用来作为 context 树的根节点。emptyCtx
,而是使用由emptyCtx
实例化的两个变量(background 、todo),分别通过调用Background
和TODO
方法得到,但这两个 context 在实现上是一样的。❝「Background和TODO方法区别:」
Background
和TODO
只是用于不同场景下:Background
通常被用于主函数、初始化以及测试中,作为一个顶层的context
,也就是说一般我们创建的context
都是基于Background
;而TODO
是在不确定使用什么context
的时候才会使用。 ❞
如果一个 Context 有子 Context,在该 Context 取消时,其下的所有子 Context 都会被取消。
Context 不仅可以发出取消信号,还可以传值,可以把它存储的值提供其他协程使用。
示例:
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
ctx, stop := context.WithCancel(context.Background())
valCtx := context.WithValue(ctx, "position","gopher")
wg.Add(2)
go func() {
defer wg.Done()
worker(valCtx, "打工人1")
}()
go func() {
defer wg.Done()
worker(valCtx, "打工人2")
}()
time.Sleep(3*time.Second) //工作3秒
stop() //3秒后发出停止指令
wg.Wait()
}
func worker(valCtx context.Context, name string){
for {
select {
case <- valCtx.Done():
fmt.Println("下班咯~~~")
return
default:
position := valCtx.Value("position")
fmt.Println(name,position, "认真摸鱼中,请勿打扰...")
}
time.Sleep(1*time.Second)
}
}
运行结果:
打工人2 gopher 认真摸鱼中,请勿打扰...
打工人1 gopher 认真摸鱼中,请勿打扰...
打工人1 gopher 认真摸鱼中,请勿打扰...
打工人2 gopher 认真摸鱼中,请勿打扰...
打工人2 gopher 认真摸鱼中,请勿打扰...
打工人1 gopher 认真摸鱼中,请勿打扰...
下班咯~~~
下班咯~~~