美文网首页
context学习笔记

context学习笔记

作者: 鸿雁长飞光不度 | 来源:发表于2021-05-21 15:10 被阅读0次

context是Go语言提供的用于控制协程的重要工具包,提供了许多开发中非常实用的功能,下面就对各种各样的context进行分析,并结合案例说明应用场景。

1. Context概述

Context其实只是一个接口,凡是实现这个接口的都可以称之为context。该接口包含四个方法,官方对每个方法的功能和职责都做了非常详细的介绍。

1.Deadline() (deadline time.Time, ok bool)

 //该函数返回代表当前context完成工作后应该被cancel的截止时间,
 //如果没有设置截止时间,ok会等于false,函数多次调用都会返回相同的结果
Deadline() (deadline time.Time, ok bool)

2.Done() <-chan struct{}

// 返回一个channel,当代表当前context完成工作后应该被cancel时,channel会被关闭。如果一个上下文
// 不能被cancel,Done就会返回nil,连续调用会返回相同结果。done函数返回的channel的关闭
// 可能会在cancel函数返回后异步执行。Done函数通常依赖select函数实现需求。
//这里可以看到更多关于Done函数cancel的案例: https://blog.golang.org/pipelines
    Done() <-chan struct{}

使用案例


//Stream函数调用DoSomething函数生成values,
//然后把这些values发给名称为out的channel,
//直到context返回一个错误或者ctx.Done的channel被关闭。
func Stream(ctx context.Context, out chan<- Value) error {
    for {
        v, err := DoSomething(ctx)
        if err != nil {
            return err
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case out <- v:
        }
    }
}
  1. Err() error
//如果Done返回的channel没有被关闭,Err函数返回nil,
//如果关闭了,Err会返回一个非nil的error解释原因:
//如果context是cancel了,error就是Canceled
//如果是截止时间到了,error就是DeadlineExceeded
//在Err返回一个非nil的error后,连续调用会得到相同的结果。
Err() error

4.Value(key interface{}) interface{}

//Value函数返回一个和当前context关联的key的value,如果没有value和key关联返回nil
//连续调用Value方法如果key相同,返回结果也相同。
//context的value仅用于请求数据的传输流程和api边界,不是用来给函数传递可选字段的。
//一个key代表了在一个context的指定的value,对于希望存储values在context里面的函数,
//通常是声明一个全局变量做key,然后把这个key作为参数,调用context.WithValue()、
//context.Value(),分别实现存值和取值。key的类型可以是任意支持相等比较任意类型
//使用者的包定义的这些key应该是非导出类型从而避免key冲突。
//使用者的包定义一个context的key应该提供一个类型安全的获取函数来说访问存储的value
Value(key interface{}) interface{}

2.Context类型

通过上面接口职责说明里面可以看到,不是所有的context都需要完全实现上面的方法,为了实现接口而去实现不需要的方法就会很多余,go语言通过一个empty类型的context解决了这个问题,下面会具体分析。

  1. emptyCtx
  • 使用:调用context.Background()方法来生成。(内部直接返回包内的全局变量emptyCtx类型的background)

  • 作用:代表一个空的context。通常用来作为生成其他类型的context的父context。

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 interface{}) interface{} {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

2.cancelCtx

  • 使用: 调用 context.WithCancel(context.Background())格式 生成。
  • 作用:是一个可以执行cancel的上下文,通过消息对协程进行统一控制。
  //初始化一个cancelCtx实例
  //如果父context也支持cancel,添加到父节点上。
 // 返回cancelCtx实例和cancel方法。
 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
 }

添加到父parent函数的实现

func propagateCancel(parent Context, child canceler) {
    done := parent.Done() //
    if done == nil {
        return //说明parent不支持cancel,直接返回吧
    }

    select {
    case <-done:
        // parent已经cancel了,当前也执行cancel,并传递父parent的err
        child.cancel(false, parent.Err())
        return
    default:
    }
        // 查找支持parent的父节点
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
                        //如果父parent支持cancel
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else { // 如果parent的的父节点都不支持cancel
        atomic.AddInt32(&goroutines, +1)// 当前包内的协程数量开启数+1
        go func() {
            select {
            case <-parent.Done(): // 等待parent节点结束
                child.cancel(false, parent.Err()) // 在把当前的结束。
            case <-child.Done():
            }
        }()
    }
}

parentCancelCtx函数实现。

// 返回父级的中最基础的*cancelCtx,通过调用parent.Value(&cancelCtxKey)
// 寻找最里面的*cancelCtx,然后检查parent.Done()是否和*cancelCtx匹配,
// 如果不匹配,说明*cancelCtx被一个自定义实现包装过了,提供了一个不同的done 
// channel,在这种情况下我们不应该绕过它。
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    done := parent.Done()
    if done == closedchan || done == nil {
        return nil, false
    }
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
    if !ok {
        return nil, false
    }
    p.mu.Lock()
    ok = p.done == done
    p.mu.Unlock()
    if !ok {
        return nil, false
    }
    return p, true
}
//  这是一个可以被取消的context,当调用Cancel的时候,所有的实现cancler接口的子context也会调用cancel
type cancelCtx struct {
// 这里是一个接口,蕴含的形式定义的,通常我们可以用emptyCtx来往这里放,
// emptyCtx完整实现了context,所以凡是这样写的都可以按重写需要的接口方法,比如
// 这个就没有重写deadLine方法,但是依然时context,其他类型的context都采用了类似形式。
    Context  
    mu       sync.Mutex            // protects following fields
    done     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
}

// 他本身
func (c *cancelCtx) Value(key interface{}) interface{} {
    if key == &cancelCtxKey { // 这个key专门返回自己
        return c
    }
    return c.Context.Value(key) // 其他的调用c.Context.Value()方法实现。
}

// c.done可能是nil,生命周期nil,chan,close
func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

// 返回错误原因
func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

cancelCtx还实现了canceler,这接口代表了可以cancel的context应该实现的方法。

type canceler interface {
        // 取消函数(是否从父节点移出,err是取消原因,不能为nil)
    cancel(removeFromParent bool, err error) // 
       // 完成以后发的消息。
    Done() <-chan struct{}
}

对cancel方法的实现

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan //全局变量
    } else {
        close(c.done)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}

使用案例

    var ctx, cancel = context.WithCancel(context.Background())
    for i := 0; i < 2; i++ {
        go func(ctx context.Context, i int) {

            go func(ctx2 context.Context,i2 int) {
                for ; ;  {
                    select {
                    case <-ctx.Done():
                        err := ctx.Err()
                        fmt.Printf("i2_1 function done value %v err %s\n", i2,err.Error())
                        return
                    default:
                        fmt.Println("i2_1 doing")
                        //time.Sleep(5*time.Microsecond)
                    }

                }
            }(ctx,i)

            go func(ctx2 context.Context,i2 int) {
                for ; ;  {
                    select {
                    case <-ctx.Done():
                        err := ctx.Err()
                        fmt.Printf("i2_2 function done value %v err %s\n", i2,err.Error())
                        return
                    default:
                        fmt.Println("i2_2 doing")
                        //time.Sleep(5*time.Microsecond)
                    }
                }

            }(ctx,i)

        }(ctx,i)
    }
    time.Sleep(100*time.Microsecond) // 模拟逻辑执行
    cancel() // 取消以后,创建的协程会收到取消通知
    time.Sleep(1000*time.Microsecond) // 模拟后续执行流程

创建了多个协程,包括嵌套创建的。然后在协程里面执行逻辑,并监听ctx.Done的消息。通过调用cancel方法每个协程都收到ctx的cancel消息了。
输出结果如下

i2_2 doing
i2_2 doing
i2_1 doing
i2_2 doing
i2_2 function done value 1 err context canceled
i2_1 function done value 0 err context canceled
i2_2 function done value 0 err context canceled
i2_1 function done value 1 err context canceled
  1. timerCtx
  • 使用:调用WithDeadline或者WithTimeout方法
  • 作用:timerCtx基于cancelCtx实现,增加了deadline用于标记自动cancel截止时间,timer就是对时间的计时器。
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

//WithDeadline返回带有一个调整了不迟于d的截止日期的父context的拷贝,如果父context截止日期
// 已经早于d,等同于返回的是parent,返回的上下文里面的Done channel会在截止时间过期、
// 或者返回的cancel函数被手动执行,或者parent的context的Done channel被关闭时关闭。
// 不论哪一个情况第一次发生都会关闭。

// canceling一个context会释放和它关联的资源,所以代码应该在完成在context的操作后立即调用。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // 当前设置的时间比parent的早,用parent的
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
              //过期
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded) //超时
        })
    }
        // 外面可以自己调用,主动调用的原因是【取消】
    return c, func() { c.cancel(true, Canceled) }
}

// WithTimeout 返回的结果是 WithDeadline(parent, time.Now().Add(timeout)).
//
// 取消context操作将会释放和它关联的资源, 所以代码里面应该尽可能早的在上线文中调用cancel:
//
//  func slowOperationWithTimeout(ctx context.Context) (Result, error) {
//      ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
//      defer cancel()  //如果slowOperation在超时之前完成释放资源。
//      return slowOperation(ctx)
//  }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

接口实现

// 直接返回截止时间
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

// 和cancelCtx差不多,只是关闭了定时器。
func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)
    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()
}

4.valueCtx

  • 使用:context.WithValue()
  • 作用:各个协程之间传递一些数据, 只需要实现context的value接口。
// WithValue returns a copy of parent in which the value associated with key is
// val.
// 设置值
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
        // key是可以用来相等比较
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
        // 会从父context去查询。
    return c.Context.Value(key)
}

使用案例,下面的案例来着官方

// user包定义了一个User类型存储在context。
package user

import "context"

// User 是存储在context里面的值。
type User struct {...}

// key定义了非导出的数据类型,仅包内可见,
// 这避免了和其他包的冲突
type key int

// userKey是代表在context里面的user.User作为value时对应的key,也是非导出的,
// 使用者应该使用 ser.NewContext和user.FromContext函数,而不是直接使用这个key
var userKey key
// 返回一个新的context并且设置一个value u.
func NewContext(ctx context.Context, u *User) context.Context {
    return context.WithValue(ctx, userKey, u)
}

// 获取存储在context里面的User类型的value。
func FromContext(ctx context.Context) (*User, bool) {
    u, ok := ctx.Value(userKey).(*User)
    return u, ok
}      

总结:

  • Context仅仅是一个接口定义,跟据实现的不同,可以衍生出不同的context类型;
  • cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
  • timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;
  • valueCtx实现了Context接口,通过WithValue()创建valueCtx实例; - - 三种context实例可互为父节点,从而可以组合成不同的应用形式;

相关文章

  • context学习笔记

    context是Go语言提供的用于控制协程的重要工具包,提供了许多开发中非常实用的功能,下面就对各种各样的cont...

  • axios 学习笔记之context-type与 'applic

    axios 学习笔记之context-type与 'application/x-www-form-urlencod...

  • Context的学习笔记

    Android应用程序开发 是采用的 JAVA 语言,为什么 Activity 不可以 new 出来? 因为 An...

  • Go context 学习笔记

    golang 的 Context包,是专门用来简化对于处理单个请求的多个goroutine之间与请求域的数据、取消...

  • Golang学习笔记-Context

    有时候一个功能可能涉及多个goroutine的调用,当该功能中途需要取消时,需要通知其它goroutine,这个时...

  • 学习笔记——React Context API

    使用场景 通常在React应用中,数据的传递是通过props属性从上到下(由父到子)进行传递(不用Redux), ...

  • golang学习笔记之:context

    context使用介绍: 对于context上下文的使用,有精力多朋友最好去阅读一下源码,源码的注释写的是真给力。...

  • golang学习笔记之-context

    Context-用来管理调用上下文,控制一个请求的生命周期。 直接看代码:Context是一个接口 Context...

  • 一篇m6A综述阅读笔记

    title: 文献笔记-Where, When, and How Context-Dependent Functi...

  • Android学习笔记7 Context完全解析

    不知不觉,Android学习笔记已经写到了第7篇,这篇主要是介绍Context,上下文,因为之前经常会碰到它,但是...

网友评论

      本文标题:context学习笔记

      本文链接:https://www.haomeiwen.com/subject/eoxbjltx.html