Golang context 实现原理与源码分析

news2024/12/26 9:25:11

0 context入门介绍

context是Golang应用开发常用的并发控制技术,主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。

context实际上只定义了接口,凡是实现该接口的类都可称为是一种context,官方包中实现了几个常用的context,分别可用于不同的场景;

  • cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
  • timerCtx实现了Context接口,通过WithDeadline()WithTimeout()创建timerCtx实例;
  • valueCtx实现了Context接口,通过WithValue()创建valueCtx实例;

context译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。以上三种context实例可互为父节点,父goroutine派生出子goroutine,而子goroutine又继续派生新的goroutine,即多级goroutine。
在Go 里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制。但是在多级goroutine的情况下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现。
在这里插入图片描述

1. Context接口

1.1 context.Context接口定义

type Context interface {
    Deadline() (deadline time.Time, ok bool)//返回 context 的过期时间;
    Done() <-chan struct{}					//返回 context 中的 channel;
    Err() error								//返回错误;
    Value(key interface{}) interface{}		//返回 context 中的对应 key 的值.
}
  1. Deadline():该方法返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,则ok == false,此时deadline为一个初始值的time.Time值
  2. Done():该方法返回一个channel,需要在select-case语句中使用,如”case <-context.Done():”。
    • 当context关闭后,Done()返回一个被关闭的管道,关闭的管道仍然是可读的,据此goroutine可以收到关闭请求;
    • 当context还未关闭时,Done()返回nil。
  3. Err():该方法描述context关闭的原因。关闭原因由context实现控制,不需要用户设置。比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同:
    • 当context关闭后,Err()返回context的关闭原因;
      • deadline超时关闭:“context deadline exceeded”;
      • cancel主动关闭: “context canceled”。
    • 当context还未关闭时,Err()返回nil;
  4. Value():有一种context即valueCtx,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息。Value()方法就是用于此种类型的context,该方法根据key值查询map中的value。
    在这里插入图片描述

2. emptyCtx

context包中定义了一个空的context, 名为emptyCtx,用于context的根节点,空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节点。

2.1 emptyCtx源码

type emptyCtx int	//emptyCtx 是一个空的 context,本质上类型为一个整型;

//Deadline 方法会返回一个公元元年时间以及 false 的 flag,标识当前 context 不存在过期时间;
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return}

//Done 方法返回一个 nil 值,用户无论往 nil 中写入或者读取数据,均会陷入阻塞;
func (*emptyCtx) Done() <-chan struct{} {return nil}

func (*emptyCtx) Err() error {return nil}//Err 方法返回的错误永远为 nil;

func (*emptyCtx) Value(key interface{}) interface{} {return nil}//Value 方法返回的 value 同样永远为 nil.

2.2 context.Background() & context.TODO()

emptyCtx通过下面两个导出的函数(首字母大写)对外公开:我们所常用的 context.Background()context.TODO() 方法。

context包提供了4个方法创建不同类型的context,使用这四个方法时如果没有父context,都需要传入emptyCtx ,即backgroud或todo作为其父节点:

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)
//context.Background()函数返回一个空的上下文对象,被视为所有上下文树的根节点,不需要传递值或取消信号。
func Background() Context {return background}
//context.TODO()函数返回一个空的上下文对象,用于该部分代码还未确定具体需要哪种上下文对象,
func TODO() Context {return todo}

3. cancelCtx

3.1 canceler接口定义

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是这两个结构体内部的指针实现了 canceler 接口。

3.2 cancelCtx 数据结构

type cancelCtx struct {
    Context							//嵌入式接口类型,cancelCtx 必然为某个context的子context;
   
    mu       sync.Mutex            // 互斥锁,保护以下字段不受并发访问的影响
    done     atomic.Value          // 原子通道,第一次调用取消函数时被惰性创建,在该context及其后代context都被取消时关闭
    children map[canceler]struct{} // 值为struct{}其实是一个set,保存当前上下文的所有子上下文,第一次取消调用时设为nil
    err      error                 // 第一次取消操作时设置为一个错误值,对此上下文及后代上下文进行取消操作返回该错误
}

这是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。
在这里插入图片描述

3.3 Done 方法

func (c *cancelCtx) Done() <-chan struct{} {
    d := c.done.Load()				//基于 atomic 包,读取 cancelCtx 中的 chan;倘若已存在,则直接返回;
    if d != nil {
        return d.(chan struct{})
    }
    c.mu.Lock()						// 加锁后,再次检查 chan 是否存在,若存在则返回;(double check)
    defer c.mu.Unlock()
    d = c.done.Load()
    if d == nil {
        d = make(chan struct{})
        c.done.Store(d)				//初始化 chan 存储到 aotmic.Value 当中,并返回.(懒加载机制)
    }
    return d.(chan struct{})
}

c.done 使用了惰性加载(lazy loading)的机制,只有一次调用 Done() 方法的时候才会被创建,且通过加锁二次检查,确保在多个goroutine同时调用 Done() 方法时,只有第一个goroutine创建通道,其他goroutine均复用已创建的通道。函数返回的是一个只读的 channel,一般通过搭配 select 来使用,当channel关闭后,就会立即读出零值,据此可以判断cancelCtx是否被取消。
在这里插入图片描述

3.4 Errl() 方法

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

cancelCtx.err默认是nil,在context被cancel时指定一个error变量: “context canceled”。

3.5 Valuel() 方法

func (c *cancelCtx) Value(key any) any {
    if key == &cancelCtxKey {		//倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针;
        return c
    }
    return value(c.Context, key)	//否则遵循 valueCtx 的思路取值返回
}

倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针(基于 cancelCtxKey 为 key 取值时返回 cancelCtx 自身,是 cancelCtx 特有的协议)

3.6 context.WithCancel()方法

context.WithCancel()方法 是Go语言中的context包提供的函数之一,用于创建一个可取消的上下文对象。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {			//校验参数parent——父context 非空;
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)	//注入parent——父context构造cancelCtx;
    propagateCancel(parent, &c)	//propagateCancel方法内启动一个守护协程,当父context终止时,该cancelCtx 也被终止;
    
    return &c, func(){ c.cancel(true, Canceled)}//将构造的cancelCtx返回,同时返回终止该cancelCtx的闭包函数cancel 
    //第一个参数是 true,也就是说取消的时候,需要将自己从父节点里删除。第二个参数则是一个固定的取消错误类型:
}

context.WithCancel()函数接受一个父上下文对象parent 作为参数,返回一个新的上下文cancelCtx对象ctx 及其对应的取消函数cancel 。当调用取消函数时,该上下文对象及其所有后代上下文对象均会被取消。

3.6.1 newCancelCtx方法

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

注入parent 父 context 后,返回一个新构造的的 cancelCtx.

3.6.2 propagateCancel方法

func propagateCancel(parent Context, child canceler) {//parent即父协程,child即当前协程
    done := parent.Done()
    if done == nil {
        return // parent 是不会被 cancel 的类型(如 emptyCtx),则直接返回
    }

    select {
    case <-done:
        //  parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err
        child.cancel(false, parent.Err())
        return
    default:
    }
	//parentCancelCtx通过 parent.Value(&cancelCtxKey)判断是否是cancelCtx 类型
	//倘若以特定的 cancelCtxKey 从 parent 中通过parent.Value()取值,取得的 value 是 parent 本身,则返回 true. 
	///倘若 parent 的 channel 已关闭或者是不会被 cancel 的类型,则返回 false;
    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{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done()://用于退出go协程
            }
        }()
    }
}

propagateCancel 方法顾名思义,用以传递父子 context 之间的 cancel 事件,向上寻找可以“挂靠”的“可取消”的 context,并且“挂靠”上去。这样,调用上层 cancel 方法的时候,就可以层层传递,将那些挂靠的子 context 同时“取消”。

当通过WithCancel(parent Context)创建一个新的cancelContext时propagateCancel 被调用,用来确保当parent父context终止时,该cancelCtx 也被终止:

  1. 若 parent 是不会被 cancel 的类型(如 emptyCtx),不需要传递,则直接返回;
  2. 若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;
  3. 若 parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中;
  4. 若 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并传递 parent 的 err

在这里插入图片描述

3.6.3 cancelCtx.cancel()

//第一个 removeFromParent 是一个 bool 值,表示当前 context 是否需要从父 context 的 children set 中删除;
//若当前的cancel是由父节点取消引起的,由于父节点已取消,则removeFromParent可以为false
//第二个 err 则是 cancel 后需要展示的错误;
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	//校验传入的 err 是否为空,若为空则 panic
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    //加锁;
    c.mu.Lock()
    //校验 cancelCtx 自带的 err 是否已经非空,若非空说明已被 cancel,则解锁返回
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    //将传入的 err 赋给 cancelCtx.err
    c.err = err
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	// //若 channel 此前未初始化,则直接注入一个 closedChan,否则关闭该 channel;
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    //遍历当前 cancelCtx 的 children set,依次将 children context 都进行 cancel;
    for child := range c.children {
        // 且此时child.cancel第一个参数为false,因为父context已经关闭,会将child从set中删除
        // 即若当前的cancel是由父节点取消引起的,则removeFromParent 则可以为false
        child.cancel(false, err)
    }
    c.children = nil
    //解锁.
    c.mu.Unlock()
	// 根据传入的 removeFromParent flag 判断是否需要手动把 cancelCtx 从 parent 的 children set 中移除.
    if removeFromParent {
    	//如果 parent 不是 cancelCtx,直接返回(因为只有 cancelCtx 才有 children set) 
		//加锁;从 parent 的 children set 中delete删除对应 child解锁返回.
        removeChild(c.Context, c)
    }
}

context.WithCancel() 函数返回一个新的上下文(context)以及一个可用于取消该上下文的取消函数c.cancel(true, Canceled)。当调用cancel()取消函数时,将通过 context 的取消信号来通知这个上下文相关联的所有操作停止执行并释放资源。步骤如下:

  1. 判断err参数是否为空,若为空则引发panic(“context: internal error: missing cancel error”),否则首先加锁
  2. 判断cancelCtx 自带的 err 是否已经非空,若非空说明cancelCtx 已被 cancel,则解锁返回
  3. 判断channel是否初始化,若未初始化,则直接注入一个 closedChan关闭的通道,否则关闭已有的channel
  4. 将所有的子contex一起取消,并解锁
  5. 加锁,从 parent 的 children set 中delete删除所有 child,解锁返回.
    在这里插入图片描述

4. timerCtx

4.1 timerCtx结构

type timerCtx struct {
    cancelCtx     // 嵌入式结构体,继承了 cancelCtx 中的所有字段和方法
    
    timer    *time.Timer // 当前 context 所关联的 Timer,在 cancelCtx.mu时间
	deadline time.Time	//预期的上下文超时时间
}

timerCtx 在 cancelCtx 基础上又做了一层封装,除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间.
在这里插入图片描述

4.2 timerCtx.Deadline()方法

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

由于继承了 cancelCtx 结构体,timerCtx 可以从其父亲结构体获得取消能力,同时也可以使用它的成员变量 timer 来设置超时。当计时器触发时,会使用与该上下文相关联的取消函数来取消该上下文中运行的所有操作。

4.3 context.WithTimeout()方法

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

context.WithTimeout 方法用于构造一个 timerCtx,本质上会调用 context.WithDeadline 方法,截止时间是time.Now().Add(timeout):

4.4 context.WithDeadline()方法

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	//校验 parent context 非空;
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    //校验 parent是否可过期, 且过期时间是否早于自己,若是,则构造一个 cancelCtx 返回即可;
    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 的 cancel 事件到子 context;
    propagateCancel(parent, c)
    
    dur := time.Until(d)
    //判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    
    //加锁; c.cancel() 和 time.AfterFunc() 两个操作的原子性和同步性确保在计时器触发前或在取消方法执行后不再触发计时器。
    c.mu.Lock()
    defer c.mu.Unlock()
    //c.err为空,即没有终止
    if c.err == nil {
    	//启动time.Timer,达到过期时间后会调用cancel终止该 timerCtx,并返回 DeadlineExceeded 的错误;
    	//time.AfterFunc(dur, func()) 函数的工作方式是异步的,它创建一个新的 Goroutine 来运行计时器并等待计时器触发。
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    // 返回 timerCtx,以及一个封装了 cancel 逻辑的闭包 cancel 函数.
    return c, func() { c.cancel(true, Canceled) }
}

WithDeadline 函数会创建一个新的到期时间戳的上下文。如果在到期时间之前完成操作,则操作正常完成;否则,如果到期时间已经过了,上下文将被标记为超时,并将其传播给与此上下文相关联的所有操作,从而达到错误处理和资源释放的目的。返回值是一个设置好到期时间戳的子上下文对象,以及一个取消函数(CancelFunc),可以随时用于删除此上下文及其所有子级上下文。创建过程如下:

  1. 判断parent是否为空,若为空,则引发panic(“cannot create context from nil parent”)
  2. 判断parent是否可过期, 且过期时间是否早于自己,若是,则构造一个 cancelCtx 返回即可;
  3. 构造出一个新的 timerCtx结构体,并调用propagateCancel启动守护方法,同步 parent 的 cancel 事件到子 context;
  4. 判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;
  5. 加锁;并启动time.Timer,这会开启一个新的协程,当Timer达到过期时间后会调用cancel终止该 timerCtx,并返回 DeadlineExceeded 的err;
  6. 返回 timerCtx,以及一个封装了 cancel 逻辑的闭包 cancel 函数.

4.5 timerCtx.cancel()方法

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	//复用继承的 cancelCtx 的 cancel 能力,进行 cancel 处理;
    c.cancelCtx.cancel(false, err)
    //判断是否需要手动从 parent 的 children set 中移除,若是则进行处理
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    // 加锁;
    c.mu.Lock()
    if c.timer != nil {
    	//停止 time.Timer
        c.timer.Stop()
        c.timer = nil
    }
    //解锁返回.
    c.mu.Unlock()
}

由于继承了 cancelCtx 结构体,timerCtx 可以从其父亲结构体获得取消能力,调用cancel()时过程如下

  1. 首先调用父亲结构体的cancel()函数,不过此时父亲结构体的cancel()第一个参数removeFromParent 为false,因为不需要删除父亲结点的所有子结点
  2. 判断是否需要将该timerCtx手动从 parent 的 children set 中移除,若是则进行处理
  3. 之后加锁,暂停timer计数器并置空,之后解锁返回

5 valueCtx

5.1 valueCtx结构

type valueCtx struct {
    Context
    key, val any
}

valueCtx 同样继承了一个 parent context; 一个 valueCtx 中仅有一组 kv 对.
在这里插入图片描述

5.2 valueCtx.Value()方法

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}
func value(c Context, key any) any {
	//启动一个 for 循环,由下而上,由子及父,依次对 key 进行匹配
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        //其中 cancelCtx、timerCtx、emptyCtx 类型会有特殊的处理方式
        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)
        }
    }
}
  • 假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value;
  • 假如不等,则调用value方法从 parent context 中依次向上寻找,直到找到根节点emptyctx,若找不到则返回空。
  • 其中 cancelCtx、timerCtx、emptyCtx 类型会有特殊的处理方式,如当前key== &cancelCtxKey ,则会返回cancelCtx自身,而emptyCtx 则会返回空

在这里插入图片描述

5.3 context.WithValue()方法

func WithValue(parent Context, key, val any) Context {
    if parent == nil {	//倘若 parent context 为空,panic;
        panic("cannot create context from nil parent")
    }
    if key == nil {		//倘若 key 为空 panic;
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {	//倘若 key 的类型不可比较,panic;
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}	//包括 parent context 以及 kv对,返回一个新的 valueCtx结构体.
}

阅读源码可以看出,valueCtx 不适合视为存储介质,存放大量的 kv 数据,原因如下:

  • 一个 valueCtx 实例只能存一个 kv 对,因此 n 个 kv 对会嵌套 n 个 valueCtx,造成空间浪费;
  • 基于 k 寻找 v 的过程是线性的,时间复杂度 O(N);
  • 不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,返回不同的 v.

由此得知,valueContext 的定位类似于请求头,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等.

6. 总结

6.1 总体类图

在这里插入图片描述

6.2 context注意事项

  1. 通常 Context 都是做为函数的第一个参数进行传递(规范性做法),并且变量名建议统一叫 ctx
  2. Context 是线程安全的,可以放心地在多个 goroutine 中使用。
  3. 当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号
  4. 不要把原本可以由函数参数来传递的变量,交给 Context 的 Value 来传递。
  5. 当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil。
  6. 当一个 Context 被 cancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。

6.3 context使用案例

6.1 context.withCancel()使用案例

package main

import (
context"
    "fmt"
    "time"
)

func HandleRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandleRequest Done.")
            return
        default:
            fmt.Println("HandleRequest running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go HandleRequest(ctx)

    left := 10
    for i := 1; i < 10; i++ {
        left -= i
        time.Sleep(500 * time.Millisecond)
        if left < 0 {
            cancel()
            break
        }
    }
    //Just for test whether sub goroutines exit or not
    time.Sleep(5 * time.Second)
}

上面代码中协程HandelRequest()用于处理某个请求,其又会创建两个协程:WriteRedis()、WriteDatabase(),main协程创建context,并把context在各子协程间传递,main协程会计算left来模拟实际剩余的资源,当剩余的资源不足时可以cancel掉所有子协程。

6.2 context.WithTimeout()使用案例

package main

import (
    "fmt"
    "time"
    "context"
)

func HandelRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    ctx, _ := context.WithTimeout(context.Background(), 500 * time.Millisecond)
    go HandelRequest(ctx)

    time.Sleep(1 * time.Second)
}

同样的WriteRedis()、WriteDatabase()应用场景,但不是通过计算剩余的资源来cancel掉所有协程,而是判断当前的写入耗时,如果写入时间过长,则可通过WithTimeout()进行cancel。

6.3 context. WithValue()使用案例

package main

import (
    "fmt"
    "time"
    "context"
)

func HandelRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running, author: ", ctx.Value("Pistachiout"))
            time.Sleep(200 * time.Millisecond)
        }
    }
}
func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running, author: ", ctx.Value("Pistachiout"))
            time.Sleep(200 * time.Millisecond)
        }
    }
}
func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running, author: ", ctx.Value("Pistachiout"))
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    ctx, _ := context.WithTimeout(context.Background(), 500 * time.Millisecond)
    ctx2 := context.WithValue(ctx, "author", "Pistachiout")
    go HandelRequest(ctx2)

    time.Sleep(1 * time.Second)
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/652449.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DataGrip使用技巧

DataGrip介绍 DataGrip是JetBrains提供的面向开发人员的数据库管理产品。提供智能查询控制台、高效的架构导航、智能SQL补全等功能。 同类的产品有navicat、dbeaver。本文中使用的DataGrip版本为2023.1 显示数据库其他类型的数据库结构 DataGrip中如果某类型数据库结构数量为…

GaussDB单SQL性能慢分析

文章目录 问题描述问题现象告警单SQL性能慢分析步骤一&#xff1a;确定目标SQL步骤二&#xff1a;收集统计信息、提前排除影响步骤三&#xff1a;分析SQL性能瓶颈 单SQL性能慢-视图分析流控导致慢SQL并发锁冲突导致慢SQL表膨胀导致大量的死元组业务语句不优、计划不优 问题描述…

8自由度并联腿机器狗实现姿态平衡

1. 功能说明 本文示例将实现8自由度并联腿机器狗保持姿态平衡的功能&#xff0c;当机器狗在一个平台上原地站立&#xff0c;平台发生倾斜时&#xff0c;机器狗能够自动调整姿态&#xff0c;保证背部水平。 2. 机器狗的稳定性分析 稳定性是机器狗运动中很重要的一部分&#xff0…

Leetcode | 35 搜索插入位置

35 搜索插入位置 文章目录 35 搜索插入位置题目我的思路[官方题解](https://leetcode.cn/problems/search-insert-position/solutions/333632/sou-suo-cha-ru-wei-zhi-by-leetcode-solution/ "官方题解")欢迎关注公众号【三戒纪元】 题目 给定一个排序数组和一个目标…

Rocky linux 9.0系统安装MySQL5.7

前言 本文将带你在Rocky linux 9.0上折腾一个MySQL5.7&#xff0c; 说干就干。 文章目录 前言安装环境下载mysql 包开始安装启动测试总结 安装环境 删除系统中可能存在的包&#xff1a; [rootmufeng ~]# rpm -qa |grep mysql [rootmufeng ~]# rpm -qa |grep mariadb [rootmu…

Ansible概述和模块解释

Ansible概述 Ansible介绍 Ansible是一个基于Python开发的配置管理和应用部署工具&#xff0c;现在也在自动化管理领域大放异彩。它融合了众多老牌运维工具的优点&#xff0c;Pubbet和Saltstack能实现的功能&#xff0c;Ansible基本上都可以实现。 Ansible能做什么 Ansible能…

Linux---详细讲解linux计算机体系结构

前言 Linux是一种开源的操作系统&#xff0c;它的核心思想是基于冯诺依曼体系结构。在本文中&#xff0c;我们将深入探讨Linux的基本原理和操作系统的概念。 Linux是一款基于Unix操作系统的开源软件&#xff0c;它的核心是由Linus Torvalds在1991年开发的。Linux的出现&#x…

认识 Vue.js

node版本管理工具 nvm - 简书 Vue介绍 Vue.js(简称Vue) 是一套用于构建用户界面的渐进式前端框架。&#xff08;其实就是使用js开发的一个框架&#xff09; Vue.js 核心实现 : 响应式的数据绑定&#xff1a;当数据发生改变&#xff0c;视图可以自动更新&#xff0c;不用关心D…

「有问必答」Go如何优雅的对时间进行格式化?

昨天 交流群 关于「Go如何优雅的对时间进行格式化?」展开了讨论&#xff1a; 咋搞捏&#xff1f; 如何在不循环的情况下&#xff0c;把列表数据结构体的时间修改为咱们习惯的格式&#xff0c;而不是UTC模式 我们要实现的效果如下: created_at 是go语言原生的方式&#xff0…

Android Framework分析Zygote进程的启动过程

Zygote进程是Android系统中的一个重要进程&#xff0c;其主要作用是预热Java虚拟机和启动应用进程。本文将着重分析Zygote进程的启动过程&#xff0c;结合代码注释和示例代码&#xff0c;让读者更好地理解Zygote的内部工作原理。 Zygote进程的启动过程 Zygote进程的启动过程包…

Python3 里面的四舍五入

目录 1.一般的四舍五入 : 使用内置的round函数 1.1官方文档&#xff1a; 1.2 举例说明&#xff1a; 2.python3里的格式化输出 format 2.1 记忆法则 &#xff1a;填齐宽 逗精类 2.2 format实质就是通过设置精度间接使用了等效round函数&#xff0c;但是不要把格式化输出和四…

chatgpt赋能python:Python是如何帮助确定location的?

Python是如何帮助确定location的&#xff1f; 什么是location&#xff1f; 在SEO中&#xff0c;location指的是特定页面、文章或者商铺在搜索结果中的排名位置。通常来说&#xff0c;更高的location意味着更多的点击率和流量&#xff0c;因此在SEO中&#xff0c;确定location…

独立站思考:Facebook选品测品

导语&#xff1a;对于独立站而言&#xff0c;获取稳定的流量是至关重要的。本文将探讨如何利用Facebook的选品测品功能&#xff0c;精准找到用户并提高点击率&#xff0c;以及如何通过数据分析优化&#xff0c;提高转化率并快速产生订单。 第一部分&#xff1a;精准定位用户的方…

Nodejs五、Express

零、文章目录 Nodejs五、Express 1、初识 Express &#xff08;1&#xff09;Express 简介 Express是什么 官方给出的概念&#xff1a;Express 是基于 Node.js 平台&#xff0c;快速、开放、极简的 Web 开发框架。 通俗的理解&#xff1a;Express 的作用和 Node.js 内置的 …

Swift 5.9 有哪些新特性(一)

文章目录 前言if 和 switch 表达式Value 和 Type 参数包 前言 虽然 Swift 6 已经在地平线上浮现&#xff0c;但 5.x 版本仍然有很多新功能-更简单的 if 和 switch 用法、宏、非可复制类型、自定义 actor 执行器等等都将在 Swift 5.9 中推出&#xff0c;再次带来了一个巨大的更…

矩阵补全文献汇总

[1] Nguyen L T , Kim J , Shim B .Low-Rank Matrix Completion: A Contemporary Survey[J].IEEE Access, 2019, PP(99):1-1.DOI:10.1109/ACCESS.2019.2928130. 几根棒子的一篇工作。基本结构可以借鉴。 适用于秩未知的矩阵补全文献汇总 [1] Fornasier M , Rauhut H , Ward…

报表岗位如何快速升职加薪?卷的心态要放平,工具要选对!

最近下班时一直看到做报表的部门每个人埋头苦干&#xff0c;不用说&#xff0c;这是又在忙半年度报告了。 现在&#xff0c;报表内卷现象十分严重&#xff0c;大家可能用的是一样的数据集&#xff0c;虽说每个人输出的报告可能结果差异不大&#xff0c;但懂得怎么利用工具&…

在HR眼里,IE证书早就不值钱了

大家好&#xff0c;我是老杨。 最近项目实在是忙&#xff0c;内容都写的少了一些&#xff0c;真的是有点力不从心的意思&#xff0c;人年纪大了&#xff0c;比不起当初年轻的自己了 和同事领导在一块儿的时间越多&#xff0c;就免不了聊到今年的就业环境。 我不提&#xff0…

Vue中如何进行代码编辑器与实时预览?

Vue中如何进行代码编辑器与实时预览&#xff1f; 在现代Web应用程序中&#xff0c;代码编辑器和实时预览已经成为了必不可少的一部分。Vue作为一款流行的JavaScript框架&#xff0c;也提供了一些工具和库&#xff0c;方便开发者在Vue中集成代码编辑器和实时预览功能。本文将介…

基于Eclipse+Java+Swing+Mysql实现超市销存管理系统

基于EclipseJavaSwingMysql实现超市销存管理系统 一、系统介绍二、功能展示1.登陆2.整体页面3.进货4.售货5.查询6、退出系统 三、数据库四、其它1.其他系统实现五.获取源码 一、系统介绍 系统实现了&#xff1a;商品进货、商品销售、库存查询 、进货查询、 售货查询、退出系统…