GO学习PART2——context
文章目录
Context
context是golang多协程的一种非常重要的工具,它是线程安全的,因此可以实现异步场景种多协程之间相互通讯的功能。同时还可以存储一定的数据在多个goroutine之间共享。
Context基本用法
对外暴露函数可以让我们构造四种基本的context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc){} // 取消类型的上下文
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc){} //定时类型的上下文
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { // 超时类型的上下文 return WithDeadline(parent, time.Now().Add(timeout)) // 本质上就是定时上下文 // 超过某个时间点 <=> 从现在开始经过多少时间 }
func WithValue(parent Context, key, val any) Context {} //可以存储数据的上下文
cancel_context的使用
- 创建多个子进程,模拟客户端撤销某个操作的时候,我们就可以停止之后所有的调用协程
- 每个run都会带一个id编号,不同id编号的完成时间也就是id对应的时间
- 当父进程任意时间取消的时候,子任务也要终止
func run(ctx context.Context, id int) {
progress := 0 //任务进度条
for {
select {
case <-ctx.Done():
fmt.Printf("我是子任务%d 被父任务终止了!\n", id)
return
default:
fmt.Printf("我是子任务%d 我正在运行\n", id)
time.Sleep(time.Second)
progress++
}
if progress == id {
break
}
}
fmt.Printf("我是子任务%d 我完成了\n", id)
}
func main() {
// 创建cancel_context context.Background()是它的父类,后面源码解读会解释
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i < 5; i++ {
go run(ctx, i)
}
time.Sleep(time.Second * 2)
cancel()
time.Sleep(time.Second * 3) //给子任务控制台输出时间
}
运行以下,结果如下:
我是子任务1 我正在运行
我是子任务4 我正在运行
我是子任务3 我正在运行
我是子任务2 我正在运行
我是子任务2 我正在运行
我是子任务3 我正在运行
我是子任务1 我完成了
我是子任务4 我正在运行
我是子任务4 我正在运行
我是子任务2 我完成了
我是子任务3 被父任务终止了!
我是子任务4 被父任务终止了!
timer_context的使用
跟cancel_context类似,但是构造context的方式不同;运行结果同上面的类似效果
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(2)*time.Second)
defer cancel() // 虽然定时上下文会自动调用cancel,但是如果我们所以任务都执行完了,也可以提前释放,防止资源的占用
// time.Sleep(time.Second * 2)
value_context的使用
实现一个简单的值传递实例
func run3(ctx context.Context, id int) {
progress := 0
for {
select {
case <-ctx.Done():
fmt.Printf("我是子任务%d 被父任务终止了!\n", id)
return
default:
fmt.Printf("我是子任务%d 父任务给我传值%s 我正在运行\n", id, ctx.Value("user"))
time.Sleep(time.Second)
progress++
}
if progress == id {
break
}
}
fmt.Printf("我是子任务%d 我完成了\n", id)
}
func main() {
ctx := context.WithValue(context.Background(), "user", "yuyuyu")
for i := 1; i < 3; i++ {
go run3(ctx, i)
}
time.Sleep(time.Second * 3)
}
运行结果如下
我是子任务2 父任务给我传值yuyuyu 我正在运行
我是子任务1 父任务给我传值yuyuyu 我正在运行
我是子任务1 我完成了
我是子任务2 父任务给我传值yuyuyu 我正在运行
我是子任务2 我完成了
Context源码分析
在context.go 的源码文件种,context本身不是一个数据结构,是一种接口。如果有struct同时实现了接口所有的方法,那么我们就可以去使用这个context
context接口
type Context interface {
Deadline() (deadline time.Time, ok bool) //用于超时和定时的context去实现自己的功能
Done() <-chan struct{} //Done传递一个chan信号值,它后面是struct{}空类
Err() error //返回context关闭的原因
Value(key any) any //用于value的context的使用,如果不存在对应的key就返回nil
}
对于这个接口我们自己不需要去实现一个context的struct,go 在内部帮我们实现了一个空类,它里面实现了四种方法,并且做了默认的操作,我们后面要想实现各种各样的context,只需要继承这个emptyCtx
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return nil
}
因此我们要去使用这个context的时候,其实就像一个树状的结构,从一个空的context可以衍生出来有各种功能的context
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
这也可以跟前面的代码联系起来,为什么我们要在parent那个参数里面写context.Background(),因为我们直接继承了根节点,因此Background这个函数也一般被用作我们最开始的根节点,但是TODO感觉没有什么特定的场景
cancel_context源码
结构如下
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}
主要的内容就是Context四个接口函数,然后mu这一把互斥锁保证安全,以及children的一个类似级联取消的效果
直接看它实现的四个函数
Deadline()
它直接默认继承emtpyCtx的实现,因为它跟时间没有关系,它不会因为时间而关闭
Value()
如果去从它这里去找存值的话,他会直接跳过,然后往它的父级去寻找;cancelCtxKey相当于是cancel类型的一个标志,int类型
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
Done()
首先通过懒加载的方式去读取context的done,如果被关闭了就返回一个chan返回值
然后要对内部元素操作,需要加一把互斥锁,对done唯一一次初始化
然后返回done值
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
Err()
加锁,然后返回err的参数,err代表的是当前context已经被取消或者超时以及其他的错误,只是代表是否被关闭
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
下面就是最主要的一个**cancel()**方法了,这个方法主要有以下几点步骤,首先就是加锁
- 如果c.err != nil,说明已经被取消了,因此不用再操作了,直接返回
- 如果没有err,就给他一个err,然后关闭done的channel返回一个信号给channel,然后对于它的所有子节点都进行级联的一个取消操作,最后再解锁
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
timer_context源码
timerCtx是cancelCtx的一个子节点,是对cancel的继承
type timerCtx struct {
*cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
Deadline()
重写了这个函数就,展示自己的截止时间
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
cancel()
因为是cancelCtx的继承,因此对于timer的cancel它先调用了父类的cancel方法,然后再去处理自己关于时间的内容
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
value_context源码
从结构体里面可以看出来,valueCtx是直接再根节点上的,且一个context只有一对key-value
type valueCtx struct {
Context
key, val any
}
直接看最关键的一个**Value()**函数
- 对于cancelCtx和timerCtx,他会直接跳过,然后找他们的父级
- 对于emptyCtx,因为是根节点,没有任何东西,直接返回nil
- 对于valueCtx,如果找的不是自生key对应的话,那么就会返回自己的父亲,进行一个向上的循环查找
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
return ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
Context使用建议
- 给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用 context.TODO
- Context 的 Value 相关方法应该传递必须的数据,不要什么都用value传递,value的检索效率是线性O(n)的
最后给出三者context的关系图