美文网首页
go context源码

go context源码

作者: 五岁小孩 | 来源:发表于2024-03-21 20:31 被阅读0次

go context源码 - Jxy 博客

一、总结

  • context 是 golang 支持的上下文,并发安全
  • context 的作用:控制 goroutine 的生命周期,同步传参
  • context 是一棵单节点树(链表),每个节点关联了父节点(最新的节点在根节点,先进后出)


    image-20230324094418293.png

二、源码

16536434298601653643429248.png

(一)context 接口


type Context interface {
    
  Deadline () (deadline time.Time, ok bool) // 返回ctx被取消的时间。 当没有设置截止日期时,截止日期返回 ok==false
  Done () <-chan struct{}               // 关闭信号
  Err () error   // 如果 Done 尚未关闭,Err 返回 nil
  Value (key any) any  // 读取context的数据,使用 WithValue 来存储数据,返回新 context
  
}

(二)emptyCtx(空实现)

实现了 context 接口,但是没有实现功能;

其中 context.Background() 和 context.TODO() 就是 emtyCtx 结构

// 实现了context接口,但是没有实现功能
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int

(三)valueCtx(携带数据的 ctx)

valueCtx目的就是为Context携带数据,他会继承父 Context

该方法的实现就是从树的最底层向上找,直到找到或者到达根 Context 为止,context 树形结构如下

[图片上传失败...(image-5abe56-1711110681663)]

// valueCtx类的创建
func WithValue(parent Context, key, val any) Context {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}


// 实现了Value方法,获取数据
func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
  // 递归从 父 context中获取数据
    return value(c.Context, 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)
        }
    }
}

(四)cancelCtx(带取消函数的 ctx)

cnacelCtx也会继承父context

cancelCtx是Go语言标准库中context包中的一个类型,表示一个可以被取消的上下文。它是context.Context接口的一个具体实现,用于在某些操作需要被取消时通知相关的goroutine;

type cancelCtx struct {
    Context 

    mu       sync.Mutex            // 一个互斥锁,用于保护接下来的字段
    done     atomic.Value          // 保存chan 类型的信道,该信道是懒加载的,即第一次调用cancel()函数时创建并关闭。
    children map[canceler]struct{} // 用于保存当前Context的子Context。第一次调用cancel()函数时,将其设置为nil
    err      error                 // 用于保存第一次调用cancel()函数时设置的错误信息
}

// 创建一个cancelCtx实例,返回一个cacelCtx实例,和一个CancelFunc函数
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
  
  // 返回一个cancelCtx实例
  // 返回一个CancelFunc函数,函数调用时执行cancel:removeFromParent=true,err=errors.New("context canceled")
    return &c, func() { c.cancel(true, Canceled) }
}
// 方法实现了 context 包中上下文的取消功能,确保所有子级也被正确地取消。
// 关闭 c.done,取消 c 的所有子级(即其派生的上下文),并且如果 removeFromParent 为 true,则将 c 从其父级的子级列表中删除.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil { // 必须有err
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil { // 如果当前的ctx已经有err,表已退出,无须执行退出
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err // 将退出的err赋值给当前ctx的err字段
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan) // 如果当前ctx的done不存在,则赋值一个默认关闭的chan
    } else {
        close(d) // 关闭chan,通知其他协程
    }
  //遍历每一个children,取消所有孩子节点
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err) // 为什么这里是false?根节点和子节点断开即可,子节点和子节点之间的联系断不断没意义
    }
    c.children = nil
    c.mu.Unlock() //解锁

    if removeFromParent {   //如果为true,则将当前节点和父节点断开链接
        removeChild(c.Context, c)
    }
}

用法

func main() {
    // 创建一个ctx
    parentCtx := context.Background()
    // 创建一个带cancel的ctx
    ctx, cancel := context.WithCancel(parentCtx)

    go func(ctx context.Context) {
      select {
      case <-ctx.Done(): // 当ctx的cancel执行之后,协程完成退出
          fmt.Println("context canceled")
      }
        
    }(ctx)
    time.Sleep(2 * time.Second)
    cancel() // 退出所有的ctx
}

(五)timerCtx(支持超时自动取消的 ctx)

timerCtx基于 cancelCtx,继承了cancelCtx只是多了一个 time.Timer和一个 deadline

timerCtx可以手动执行Cancel函数也可以在 deadline到来时,自动取消 context。

WithTimeout和WithDeadline都会创建一个timerCtx,

不同的是WithDeadline表示是截止日期(几号几点结束),WithTimeout是超时时间(多少时间后结束)

// 继承cancelCtx,并增加了 timer 和 deadline
type timerCtx struct {
    cancelCtx
    timer *time.Timer // 定时器

    deadline time.Time // 截止日期
}
// 该方法返回一个带有超时时间的timerCtx。与WithDeadline方法不同的是,WithTimeout方法的参数是一个持续时间(Duration),而不是一个时间点
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}
// 该方法返回一个带有截止时间的timerCtx。如果在截止时间之前没有取消Context,定时器会自动触发取消操作
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    //当父节点的截至时间早于当前节点的结束时间,就不用单独处理该子节点,因为父节点结束时,父节点的所有子节点都会结束=
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    //创建timerCtx实例
    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) }
}

// 实现了cancel
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()
}

用法

func main() {
  // 2秒后超时关闭
    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second))
    defer cancel() // return时自动关闭

    select {
    case <-ctx.Done():// 2秒后会执行
        fmt.Println("Context cancelled")
    case <-time.After(3 * time.Second): // 3秒的定时器,因为ctx设置的2秒,所以不会执行
        fmt.Println("Time out")
    }
}

三、面试常见问题

1. 为什么context是并发安全的

  • context在存数据时是通过new一个新的ctx,并且关联父ctx的方式,取数据时遍历整颗树匹配数据,不存在数据并发读写的问题
  • 详细看WithValue() 和 Value()源码

2. context的参数数据是如何存储的

使用context.WithValue(ctx,key,value),创建一个新的valueCtx节点,关联父节点

有劳各位看官 点赞、关注➕收藏,你们的支持是我最大的动力!!!
接下来会不断更新 golang 的一些底层源码及个人开发经验(个人见解)!!!
同时也欢迎大家在评论区提问、分享您的经验和见解!!!

相关文章

  • golang context 2022-04-20

    本文对golang context的源码进行解读,go version is go1.16.4 linux/amd...

  • Go context源码解析

    在上一篇文章 golang context初探 中,已经初步了解了context的用法以及应用的场景。那么接下来深...

  • go context包源码分析

    context包以及包内方法用以维护一组goroutine间的生命周期的截止,以及同生命周期内的共享变量本文面向有...

  • GO 语言context.Context类型

    context.Context类型 context.Context类型(以下简称Context类型)是在Go 1....

  • Golang Context 详细原理和使用技巧

    Golang Context 详细原理和使用技巧 Context 背景 和 适用场景 Context 的背景 Go...

  • go context

    Req-->a-->c--->b---->return (链式调用) 链式调用和我们普通的函数调用是存在差别的:函...

  • Go context

    goroutine要处理的问题? 数据共享 释放回收 github练习代码 context doc blog...

  • Go context

    Go context 在RPC或者Web服务中,当Server端接受一个request的时候,都会开启一个额外的g...

  • Go context

    控制并发有两种经典的方式:WaitGroup和Context WaitGroup:控制多个Goroutine同时完...

  • go context

    context 很重要,总体作用就是 设置程序执行的 deadline 设置程序的截止日期 status...

网友评论

      本文标题:go context源码

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