GO学习PART2——context

GO学习PART2——context

Context

context是golang多协程的一种非常重要的工具,它是线程安全的,因此可以实现异步场景种多协程之间相互通讯的功能。同时还可以存储一定的数据在多个goroutine之间共享。

Context基本用法

对外暴露函数可以让我们构造四种基本的context

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc){} // 取消类型的上下文
    
  2. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc){} //定时类型的上下文
    
  3. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { // 超时类型的上下文
    	return WithDeadline(parent, time.Now().Add(timeout)) // 本质上就是定时上下文
        // 超过某个时间点 <=> 从现在开始经过多少时间
    }
    
  4. func WithValue(parent Context, key, val any) Context {} //可以存储数据的上下文
    
cancel_context的使用
  1. 创建多个子进程,模拟客户端撤销某个操作的时候,我们就可以停止之后所有的调用协程
  2. 每个run都会带一个id编号,不同id编号的完成时间也就是id对应的时间
  3. 当父进程任意时间取消的时候,子任务也要终止
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()**方法了,这个方法主要有以下几点步骤,首先就是加锁

  1. 如果c.err != nil,说明已经被取消了,因此不用再操作了,直接返回
  2. 如果没有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()**函数

  1. 对于cancelCtx和timerCtx,他会直接跳过,然后找他们的父级
  2. 对于emptyCtx,因为是根节点,没有任何东西,直接返回nil
  3. 对于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使用建议

  1. 给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用 context.TODO
  2. Context 的 Value 相关方法应该传递必须的数据,不要什么都用value传递,value的检索效率是线性O(n)的

最后给出三者context的关系图

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值