Golang context 实现原理与源码解读

news2024/11/28 14:27:54

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使用案例

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/626665.html

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

相关文章

【Android】WMS(一)Window的类型和标志

Window、WindowManager、WMS区别 Window&#xff1a; Window 是 Android 中的一个视图容器&#xff0c;代表整个屏幕或 Activity 的一部分。每个 Window 都有自己的 Surface 对象&#xff0c;Surface 对象具有绘制和渲染功能&#xff0c;可以显示 View 和其他元素。在 Android…

Vue 的基础知识 - 判断, 循环, 事件, 通信

目录 1. 了解 Vue 七大常用属性 2. 文本插值 2.1 v-bind 绑定元素 3. 判断 4. 循环 5. 事件 6. 双向绑定 7. 自定义组件 8. Axios 异步通信 8.1 什么是Axios 8.2 为什么要使用 Axios 8.3 Axios 的简单应用 8.4 Vue 的生命周期 1. 了解 Vue 七大常用属性 1. el 属性…

Qt扫盲-Bar柱状图理论

Bar柱状图理论 一、QAbstractBarSeries1. 常用设置2. 管理 BarSet 二、QBarSerie 和 QHorizontalBarSeries1. 垂直柱状图2. 水平柱状图 二、QPercentBarSeries 和 QHorizontalPercentBarSeries1. 垂直比例图2. 水平比例图 三、QStackedBarSeries 和 QHorizontalStackedBarSerie…

忠诚之源:如何获得铁粉?

文章目录 一、铁粉二、如何吸引和留住铁粉的想法2.1 了解你的目标铁粉2.2 提供有价值的内容2.3 建立良好的关系2.4 持续优化和创新 三、具体可采用的一些方法3.1 提供优质内容3.2 社交媒体互动3.3 创建独特的品牌形象3.4 提供特殊待遇和奖励3.5 参与社区和活动3.6 持续互动和更…

如何把视频语音转换成文字,分享给大家几个免费的方法!

在日常工作和学习中&#xff0c;有时需要将视频中的语音转录为文字&#xff0c;以便整理成文稿或进行其他用途。手动打字效率低下且耗时费力&#xff0c;那么如何快速将语音转换为文字呢&#xff1f;下面介绍几种简单高效的方法&#xff0c;其中之一是使用记灵在线工具。 方法…

PHP的流程控制语句

一.流程控制语句 1.if语句 PHP的if语句格式如下 if(表达式) 语句; 如果表达式的值为真&#xff0c;那么久顺序执行语句&#xff1b;否则&#xff0c;就会跳过该条语句&#xff0c;再往下执行。如果要执行的语句不指一条则用{}&#xff0c;{}被称为语句组&#xff0c;格式如…

Immer编写简洁的更新state逻辑

react官网推荐库use-immer&#xff1a;https://www.npmjs.com/package/use-immer 引入&#xff1a;import { useImmer } from "use-immer"; 优点&#xff1a; 简化代码: 只需要关注需要变动的部分&#xff0c;而 immer 本身将在后台处理其余部分。学习成本和替换代…

双碳目标下DNDC模型建模方法及在土壤碳储量、温室气体排放、农田减排、土地变化、气候变化中的实践应用

查看原文>>>双碳目标下DNDC模型建模方法及在土壤碳储量、温室气体排放、农田减排、土地变化、气候变化中的实践应用 目录 第一讲、DNDC模型介绍 第二讲、DNDC初步操作 第三讲、遥感和GIS基础 第四讲、DNDC气象数据 第五讲、DNDC土地数据 第六讲、DNDC土壤数据 …

【C语言】算法学习·Dijkstra算法详解

目录 Dijkstra算法设计 Dijkstra算法简介 Dijkstra算法的基本思想 Dijkstra贪心策略 完美图解 伪代码详解 完整代码 算法解析及优化拓展 ​使用优先队列的完整代码 Dijkstra算法设计 Dijkstra算法简介 Dijkstra算法是解决**单源最短路径**问题的**贪心算法** …

2021年国赛高教杯数学建模C题生产企业原材料的订购与运输解题全过程文档及程序

2021年国赛高教杯数学建模 C题 生产企业原材料的订购与运输 原题再现 某建筑和装饰板材的生产企业所用原材料主要是木质纤维和其他植物素纤维材料,总体可分为 A&#xff0c;B&#xff0c;C 三种类型。该企业每年按 48 周安排生产&#xff0c;需要提前制定 24 周的原材料订购和…

如何在 javascript 中按属性值查找数组中的对象

文章目录 使用 find() 方法按属性值在数组中查找对象使用 filter() 方法按属性值查找数组中的对象使用 JavaScript for 循环按属性值查找数组中的对象使用 JavaScript for...in 循环按属性值查找数组中的对象 数组指的是值的有序列表&#xff0c;每个值称为由索引指定的元素。 …

这所西安的985专硕爆冷,保护一志愿,过线即上岸!

本期为大家整理热门院校“西北工业大学”的择校分析&#xff0c;这个择校分析专题会为大家结合&#xff1a;初试复试占比、复试录取规则&#xff08;是否公平&#xff09;、往年录取录取名单、招生人数、分数线、专业课难度等进行分析。希望能够帮到大家! –所有数据来源于研招…

Plot、Scatter、Subplot函数用法

目 录 一、Plot()函数 二、Scatter()函数 三、Subplot()函数 一、Plot()函数 格式&#xff1a;matplotlib.pyplot.plot(x,y,format_string.**kwargs) 说明&#xff1a; x:x轴数据&#xff0c;列表或数组&#xff0c;可选(注&#xff1a;当绘制多条曲线时&#xff0c;不能省…

IT 系统巡检必须关注的指标总结

1. 系统整体架构 以下内容作为基本 IT 系统信息被首先调查记录&#xff0c;供分析参考使用。 ● 网络设备配置 ---设备型号, IOS 版本, 模块型号和数量,用途 ● 存储系统配置 ---设备型号, IO 带宽, Cache 容量&#xff0c;磁盘数量&#xff0c;接入模式&#xff0c;存储容…

Go项目配置管理工具---Viper

目录 Viper概述前言功能viper配置优先级 从Viper中获取值读取配置文件注册和使用别名 把值写入Viper设置默认值使用Set方法设置值把配置信息写入配置文件从io.Reader中读取配置信息到viper 监控Viper文件 Viper概述 前言 对于现代应用程序&#xff0c;尤其大中型的项目来说&a…

【MySQL】Mycat

文章目录 什么是Mycat为什么要用Mycatmycat能干什么各数据库中间件对比Mycat原理数据库中间件逻辑库逻辑表分片表分片规则全局表ER表非分片表分片节点节点主机mycat安装mycat核心配置schema.xmlserver.xmlrule.xml加密明文密码&#xff08;可选&#xff09; MyCat读写分离垂直拆…

OpenCV中的图像处理3.11(10) OpenCV中的图像变换

目录 3.11 OpenCV中的图像变换3.11.1 傅里叶变换目标理论Numpy中的傅里叶变换OpenCV中的傅立叶变换DFT的性能优化为什么Laplacian是一个高通滤波器&#xff1f;其他资源 翻译及二次校对&#xff1a;cvtutorials.com 编辑者&#xff1a;廿瓶鲸&#xff08;和鲸社区Siby团队成员&…

2.3 YARN伪分布式集群搭建

任务目的 重点掌握 YARN 集群的相关配置学会启动和关闭 YARN 集群的两种方式能够使用 jps 命令查看进程的启动情况能够通过 UI 查看 YARN 集群的运行状态任务清单 任务1:YARN 集群主要配置文件讲解任务2:YARN 集群测试任务步骤 任务1:YARN 集群主要配置文件讲解 1.1 配置环…

基于多尺度图神经网络的流场预测,实现精度与速度的平衡

项目简介 本项目来源于飞桨AI for Science共创计划的论文复现赛题&#xff0c;复现论文为《AMGNET: multi-scale graph neural networks for flow field prediction》。该论文主要采用图神经网络&#xff0c;因为在计算流体力学中计算域被网格离散化&#xff0c;这与图结构天然…

将PDF1页分割为4页

运行效果 原始PDF 分割后PDF 一、python代码&#xff08;用的是python3.9.0版本&#xff09; import os import tempfile from pdf2image import convert_from_path from PIL import Image from PyPDF2 import PdfReader, PdfWriterdef split_pdf_page(pdf_path, output_path…