Context(上下文)
前言
Context
是go语言中所提供的一种并发控制的解决方案,相比于管道与WaitGroup
,Context
可以更好的控制子孙协程以及层次更深的协程。Context
本身是一个接口,只要我们实现了该接口都可以被称为上下文,context标准库本身也提供了几个实现:
- emptyCtx
- cancelCtx
- timerCtx
- valueCtx
什么是Context
在看Context
的具体实现之前,先来看看Context
接口的定义.
go
type Context interface{
Deadline(deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
我们来看一看这个接口里面所定义的四个方法:
- Deadline
该方法有两个返回值,deadline
是截止时间,也就是上下文截止的时间,第二个值是是否设置dedline
,如果没有则一直为false
. - Done
返回值是一个空结构体的只读管道,该管道仅仅起到通知作用,不传递任何数据,当上下文所做的工作要取消的时候,该通道就会被关闭,对于一些不支持取消的上下文,可能会返回nil - Err
该方法会返回一个error
,用来表示上下关闭的原因,如果管道没有关闭,则返回nil,如果关闭的话,则返回一个error
,用来表示上下文关闭的原因。 - Value
该方法返回对应的键值,如果key不存在,或者不支持该方法,就会返回nil。
下面我们来看一个简单的例子:
go
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wa sync.WaitGroup
var stop bool
var RW sync.RWMutex
func cpuIInfo(ctx context.Context) {
defer wa.Done()
for {
select {
case <-ctx.Done():
fmt.Println("cpu info exit")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("cpu info")
}
}
}
func main() {
wa.Add(1)
ctx, cancel := context.WithCancel(context.Background())
go cpuIInfo(ctx)
time.Sleep(6 * time.Second)
cancel()
fmt.Println("main exit")
wa.Wait()
}
输出为:
go
cpu info
cpu info
main exit
cpu info
cpu info exit
或许现在你不是很清楚上面的例子,但是看完今天的博文以后,相信大家就能和好的理解了话不多说,让我们来看一下有关context的具体内容:
emptyCtx
顾名思义,emptyCtx
指的就是空的上下文,context
包下所有的实现其实都是不对外暴露的,所以我们无法直接创建context.Context
,但是go语言提供了对应的函数区创建上下文,例如下面我们可以利用context.Background()
或context.TODO()
函数来创建一个空的上下文:两个函数的具体实现如下:
go
var{
background =new(emptyCtx)
todo =new(emptyCtx)
}
func Background()Context{
return background
}
func TODO()Context{
return todo
}
我们再来看看emptyCtx
四个函数的实现:
go
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func () Done() <-chan struct{} {
return nil
}
func () Value(key any) any {
return nil
}
func (emptyContext) Err() error {
return nil
}
我们仔细观察emptyCtx的实现,发现其实emptyCtx仅仅返回了emptyCtx
指针.emptyCtx
的底层类型是int,之所以不使用空结构体,在之前我们提到过空结构体没有字段,不占用内存,但是我们要求emptyCtx
的实例都要有自己的内存地址,而在它的方法中,由于它不能被取消,所以它没有deadline
,由于它不能被取值,所以它实现的方法都是返回nil.emptyCtx通常是用来当作最顶层的上下文,在创建其他三种上下文时作为父上下文传入。
valueCtx
valueCtx
的实现比较简单,它的内部只包括一对键值对,和一个内嵌的Context
字段:
go
type valueCtx struct{
Context
kay,value any
}
它自身也实现了Value
方法,基本逻辑其实也很简单:找不到就去喊爸爸(去父上下文找):
go
func (c *valueCtx) Value(key any) any{
if c.key==key{
return c
}
return value(c.Context,key)
}
我们可以来看一个简单的例子:
go
package main
import (
"context"
"sync"
"time"
)
var w sync.WaitGroup
func main() {
w.Add(1)
go Do(context.WithValue(context.Background(), 1, 2))
w.Wait()
}
func Do(ctx context.Context) {
ticker := time.NewTimer(2 * time.Second)
defer w.Done()
for {
select {
case <-ticker.C:
println("time out")
return
case <-ctx.Done():
default:
println(ctx.Value(1).(int))
}
time.Sleep(100 * time.Millisecond)
}
}
输出为:
go
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
time out
valueCtx多用于在多级协程中传递一些数据,无法被取消,因此ctx.Done永远会返回nil,select会忽略掉nil管道。
cancelCtx
cancelCtx
以及timerCtx
都实现了canceler接口,接口类型如下:
go
type canceler interface {
// removeFromParent 表示是否从父上下文中删除自身
// err 表示取消的原因
cancel(removeFromParent bool, err, cause error)
// Done 返回一个管道,用于通知取消的原因
Done() <-chan struct{}
}
我们查看上面的源码可以看出来:cancel方法本身不对外暴露,但是会在我们创建上下文的时候通过闭包来将其封装成返回值供外界调用,这个在源码中也有所体现:
go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent==nil{
panic("cannot create context from nil parent")
}
c:=newCancelCtx(parent)
//尝试将自身添加进父级的children中
propagateCancel(parent,&c)
return &c,func(){
c.cancel(true,context.Canceled,nil)
}
}
cancelCtx
我们可以理解为一个可取消的上下文,它在创建的时候如果父级实现了canceler,就会将自身添加进父级的children中,否则就一直向上查找。如果所有的父级都没有实现canceler,就会启动一个协程等待父级取消,然后当父级结束时取消当前上下文。当调用cancelFunc时,Done通道将会关闭,该上下文的任何子级也会随之取消,最后会将自身从父级中删除。下面我们来看个例子:
go
package main
import (
"context"
"fmt"
"sync"
"time"
)
var w sync.WaitGroup
func main() {
bkg := context.Background()
ctx, cancel := context.WithCancel(bkg)
w.Add(1)
go func(ctx2 context.Context) {
defer w.Done()
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("等待取消中...")
}
time.Sleep(time.Millisecond * 200)
}
}(ctx)
time.Sleep(time.Second * 3)
cancel()
w.Wait()
}
输出为:
go
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
等待取消中...
context canceled
timerCtx
相对于cancelCtx
,timerCtx
多了超时机制,context
包下提供了两种创建的函数:
go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
这两个函数的功能类似,前者是指定一个具体的时间而后者则是指定一个时间间隔。timeCtx会在时间到期后自动取消上下文,取消的流程除了要额外的关闭timer之外,基本与cancelCtx一致,我们来看一个简单的示例:
go
package main
import (
"context"
"fmt"
"sync"
"time"
)
var w sync.WaitGroup
func main() {
w.Add(1)
deadline, ctx := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
defer ctx()
go func(ctx2 context.Context) {
defer w.Done()
for {
select {
case <-deadline.Done():
fmt.Println("上下文取消")
return
default:
fmt.Println("等待取消")
}
time.Sleep(1 * time.Second)
}
}(deadline)
w.Wait()
}
WithTimeout其实与WithDealine非常相似,它的实现也只是稍微封装了一下并调用WithDeadline,和上面例子中的WithDeadline用法一样,如下:
go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
注意 :
就跟内存分配后不回收会造成内存泄漏一样,上下文也是一种资源,如果创建了但从来不取消,一样会造成上下文泄露,所以最好避免此种情况的发生。