Go系列 --Context 解析

序言:

关于 go的并发编程

 

正文

前言需知, channel 的使用  https://blog.csdn.net/dongjijiaoxiangqu/article/details/109642733

 

1.context 的使用场景

  1.  context 的含义,context 意为上下文, go中的context 的主要使用场景为在多个goroutine 中进行 通信,避免浪费无用的计算资源。

打个比方:

 比如说一个 网络请求中, 系统起了一个 go routine A来服务此请求, 同时 A routine 中,同时需要进行多个任务,那么同时起了 B,C,D 三个 go routine 来进行处理,

而网络请求可能有一个超时处理时间,又或者是客户端掐断了网络请求,这时候 A 得到了请求中断的通知,需要将 这个信息带给 B,C,D 三个 go routine, 让它们中断处理,节省资源,此时就可以通过context 进行信息传递。

 

 

2. context 的类型

  1. context 首先是一个接口, context 体系都是以这个 接口对外暴露方法的

 

type Context interface {
	// 是否设置了超时时间
	Deadline() (deadline time.Time, ok bool)

	// 是否是已经做完
	Done() <-chan struct{}

	// 是否有错误发生
	Err() error

	// 是否带有value
	Value(key interface{}) interface{}
}

 

context 的类型

首先是 空类型的context,使用场景不大,但是其他context 构建的时候需要父context

空的context : backgound, todo

 

其次是,有实际意义的context,例如 带有cancel 的context, 带有超时的context

// 返回一个context, 一个cancelFunc ,可以通过cancelFunc 通知取消
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {

}

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

func WithValue(parent Context, key, val interface{}) Context {
	
}

 

四种类型的context:

  1. WithCancel  带取消 func 的context
  2. WithDeadline 带deadline 超时的context
  3. WithTimeout 同样也是带超时的context
  4. WithValue 能在不同context 进行值传递的context

3. 说完类型,下面我们说一说这几种context的使用方式

  1.  回到上面的使用场景,我们要想想这个组件为解决何事而生,没有它的时候,我们是怎么解决这个问题的,如果有了这个组件是不是解决此类问题,更加便捷了。

    回看 context的使用场景,我们知道了context的使用场景。

这种的话,我们很容易想到不同 routine 之间的通信方式 channel, 通过channel的相互信息传递,我们可以做到 不同routine之间的信息传递。其实context 就是 channel的一类封装,使其使用更加便利。

 

思维扩散:

假如在不同语言中,我们的解决方案又是什么呢?

java:

在java中,我们在一个线程中,另起一个线程进行计算,当我们想取消 另一个线程的计算,我们会采用 将计算线程提交到线程池,返回 Future 的方式来控制,通过 Futrue 我们可以取消,超时控制的方式控制另一个线程的运行。我们以此思考再思考下本质,

但在java中,我们调用 future的 cancel 并不能立马暂停线程的执行,只是把当前Thread.currentThread().isInterrupted() 置为true,暂停当前线程代码,还得业务方进行判断。

经过实验是这样,实验代码

 

/**
 * @author: liudeyu
 * @date: 2020/11/11
 */
public class InterruptThreadTest {

    ExecutorService executor = Executors.newFixedThreadPool(4);

    public static class AThread implements Callable<Integer> {


        @Override
        public Integer call() throws Exception {
            int index = -1;
            int reverCount = 1000000;

            // isinterrupted 判断才能生效
            while (index < 1000/* && !Thread.currentThread().isInterrupted()*/) {
                index++;
                System.out.println(index);
//                Thread.sleep(1000);

                Node tmpList = NodeUtils.createRandomeIntList(reverCount, true);
                Node tmpList2 = NodeUtils.createRandomeIntList(reverCount, true);

                MergeKSortLinkedList mergeKSortLinkedList = new MergeKSortLinkedList();
                mergeKSortLinkedList.mergeKLinkedList(new Node[]{tmpList, tmpList2});
            }
            System.out.println("being interupt");
            return 1;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        InterruptThreadTest interruptThreadTest = new InterruptThreadTest();
        Future<Integer> future = interruptThreadTest.executor.submit(new AThread());
        Thread.sleep(5000);
        future.cancel(true);
        System.out.println("cancel done");
//        interruptThreadTest.executor.shutdown();
//        try {
//            Integer tmpRes = future.get(300, TimeUnit.MICROSECONDS);
//
//            System.out.println(tmpRes);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        } catch (ExecutionException e) {
//            e.printStackTrace();
//        } catch (TimeoutException e) {
//            e.printStackTrace();
//        }finally {
//            future.cancel(true);
//        }
    }
}

 

所以其暂停其他线程的本质是通过更改变量,检测共享变量的值,来进行线程是否需要退出。

在 nodejs ,由于是单线程模型,但是单线程的任务调度,可能还是需要共享变量来进行判断。

而go中利用的是 CSP 管道通知的方式,来进行多个routine之间的通信。

 

 

3. context 的具体使用

 

func backgroundContextUse() {
	tmpCtx := context.Background()
	deadLineCtx,cancelFun:=context.WithDeadline(tmpCtx,time.Now().Add(time.Second*3))

	go func(a context.Context) {
		fmt.Printf("I am doing some job\n")
		select {
		case <-a.Done():
			fmt.Printf("task done ,exit\n")

		}
	}(deadLineCtx)

	time.Sleep(14 * time.Second)
	cancelFun()
	fmt.Printf("cancel fun")
}

func main() {
	//cancelCtxUse()
	//testDefer()
	backgroundContextUse()
	time.Sleep(time.Minute)

}

 

输出情况为:

 

I am doing some job
task done ,exit
cancel fun

 

解释:

如上述代码可以看出,这里声明了一个具有deadline 的context,在指定到期时间后就会通知相应goroutine, 就是其中的

select {

case ←  a.Done():

     省略

}

同时声明deadline context 返回 cancel 函数,可以调用 cancel 函数, 相应协程也会通知 goroutine,也就是 context.Done()也会通知到各个协程

 

cancelContext() 和 timeoutContext() 也是比较简单的使用,看其函数声明即可知

 

valueContext 是可以传递 key value的context 具体中使用场景较少

 

4. context 的实现原理

我们知道 context 是 channel的封装,我们来看下,其具体实现

首先我们看下 cancelContext 的具体实现,因为其他的 deadlineContext 和 timeOutContext 也是八九不离十

从其构造函数开始说起

 

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

 

解释:

首先要求一个parentContext, 不能为nil, 然后根据此结构,我们可以猜测 cancelContext 也可以声明cancelContext ,它是一个树形结构,这个的含义就是 父节点 调用了cancel() 以后,子节点也能自动取消其他goroutine的计算

这里简单给个例子,我们先来看一段示例代码

 

func mutipleCancelContextUsage() {
	paCancelCtx, paCanFun := context.WithCancel(context.Background())

	subCancelCtx, _ := context.WithCancel(paCancelCtx)
	subCancelCtx2, canFun2 := context.WithCancel(paCancelCtx)

	grandSubCtx3, _ := context.WithCancel(subCancelCtx2)
	var testSubContextUsageFun = func(tmp context.Context, tag string) {
		fmt.Printf(tag+"   doing job %v\n", tmp)
		select {
		case <-tmp.Done():
			//fmt.Printf("job done signal trigger\n")
			fmt.Println(tag, "  ", tmp.Err())

		}
	}

	go testSubContextUsageFun(subCancelCtx, "subCancelCtx")
	go testSubContextUsageFun(subCancelCtx2, "subCancelCtx2")
	go testSubContextUsageFun(grandSubCtx3, "grandSubCtx3")

	time.Sleep(5 * time.Second)
	canFun2()
	time.Sleep(15 * time.Second)
	fmt.Println("after 15 sec")

	paCanFun()

}
func main() {
	//cancelCtxUse()
	//testDefer()
	//backgroundContextUse()
	mutipleCancelContextUsage()
	time.Sleep(time.Minute)

}

 

上面这段代码输出情况为:

 

subCancelCtx   doing job context.Background.WithCancel.WithCancel
grandSubCtx3   doing job context.Background.WithCancel.WithCancel.WithCancel
subCancelCtx2   doing job context.Background.WithCancel.WithCancel
subCancelCtx2    context canceled
grandSubCtx3    context canceled
after 15 sec
subCancelCtx    context canceled


解释:

首先我们可以看到 上面的代码演示了不同 cancelContext 经过不同parent衍生出来的,每个parent能cancel 控制其子孙类

也就是

                                                 

                                 -   subCancelCtx       -   grandSubCtx3

paCancelCtx     

                                -   subCancelCtx2

 

可以看到 输出 context 的字符串时候,可以看到  

subCancelCtx : context.Background.WithCancel.WithCancel  是 background → cancelContext → cancelContext 连在一起

到 grandSubCtx3 就是 三个连在一起了 context.Background.WithCancel.WithCancel.WithCancel

 

回到主题,这样的树形结构的便利就在于,我们想要结束其中一个分支及其衍生分支的 产生的goroutine的运行时候,而不影响其他分支的运行时候 ,可以直接调用父节点的 cancel 方法,而相比原始channel ,我们要做到这一点就麻烦很多,最后产生的可能也是 这个context 结构

 

 

4.2 context 的源码解析

 

 

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

先看下 newCancelCtx 是怎么实现的

 

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}


type cancelCtx struct {
	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
}

type Context interface {

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

	
	Done() <-chan struct{}


	Err() error

	
	Value(key interface{}) interface{}
}

 

可以看到 cancelCtx 实现了 Context 接口,不难理解,因为CancelCtx 也是 Context 的一个实现类

这段的主体无疑就是 cancelCtx 这个结构体,我们简单看下,其实现的接口

 

其中知道的比较明显的在此不表,我们知道其继承了 Context 接口了,那么在此基础上,我们能进行cancel() 调用,其重点应该另有接口,这时候我们的目光就被这个canceler 吸引了,我们简单看下其实现。

 

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}
}

好,这下引出了 重点, cancel 是怎么实现的,以及 Done() <-chan struct{} 的又一个接口,与Context 里面的一个 函数名相同。

根据接口重点,我们依次看下 cancel 是怎么实现 Context 和 canceler 接口的

 

func (c *cancelCtx) Value(key interface{}) interface{} {
	if key == &cancelCtxKey {
		return c
	}
	return c.Context.Value(key)
}

// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int

Value 函数, 主要是拿来取值的,如果key 是固定的cancelCtxKey ,就直接返回自身

还有比较简单的接口

 

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
}

 

返回相关的字段信息,deadline 接口默认继承 parent 的,不实现

接下来就是 canceler 里面的  

 

// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})


// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
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
   // 如果 done 为 nil ,还没进行赋值
    if c.done == nil {
        c.done = closedchan
    } else {
       // 通知 channel done,通知所有持有的goroutine, close()比较特别,记得前面章节提的吗,这里的close能进行广播通知,所有持有的goroutine
        close(c.done)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        // 递归取消child
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

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

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
    p, ok := parentCancelCtx(parent)
    if !ok {
        return
    }
    p.mu.Lock()
    if p.children != nil {
        delete(p.children, child)
    }
    p.mu.Unlock()
}

我们可以看到函数注释,说得比较明确了,这个函数的意义就是取消父节点和其依存的子节点的gorutine,通知各个goroutine 的 context.Done() channel 通知, 然后加上 是否把自己从其父节点删除(ps ,在代码里面加了注释解释)

我觉得这个函数还是比较好懂,清晰的。

 

回到前面,我们还记得这个函数的调用处吗,这里简单回顾下

 

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")

看到了吗,返回给用户的cancel 函数里面 调用了这个 真正的cancel 函数。

 

 

 

看完了 cancel 方法,回到构造函数那里,我们需要看到,context 如何和它的子context 联系起来呢,这里就是这个 propagateCancel实现了,我们看其实现

把解释写进代码注释里面

func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
   // 为nil 的话,说明 父 context 是 background context 或者其他 empty ctx类型
    if done == nil {
        return // parent is never canceled
    }

    select {
    case <-done:
        // parent is already canceled
      // 这里意义比较明显,就是 parent done 的时候,把 son 节点的 也cancel 掉, 记得这里 cancel context 关的时候,会使用close(cancelCtx.Done) ,这里的话,所有的子routine 都会被通知到
        child.cancel(false, parent.Err())
        return
    default:
    }

    
    if p, ok := parentCancelCtx(parent); ok {
     // 如果parent ctx 为 cancelCtx 
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled, 这里容易理解,parent ctx 都被关掉了,err !=nil 表示为 parent ctx 被cancel 掉了,cancel 掉当前 child
            child.cancel(false, p.err)
        } else {
           // 判断是否初始化过 children map
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
       // 当前如果不是cancelCtx 情况,如果检测到 parent done,也cancel child ,感觉和上面逻辑有重复之处,可能是自定义的context 时候,也要注意监测
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}


// 这个函数返回 传入的 context 是否为 cancelCtx, 注意的是 timeoutCtx 也是继承了 cancelCtx
// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
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
}

 

整体来说cancelCtx 就已经讲完,然后 timeCtx 和 cancelCtx 类似,但多了一个 time.After 的超时监控,看下timeCtx ,你就可以理解,为啥cancelCtx是关键, 继承了 cancelCtx,所以cancelCtx 的功能,timeCtx都有,还有了一个超时功能

 

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

 

 

结尾

go的context 就介绍到这,以较为详细篇幅讲述了 这个context 这个组件,树形结构的设计, 依靠 树形结构,和 close(channel)广播通知,实现了 父子节点的 gorutine关系的通知,同时达到了,序言说的目的,节省计算资源的目的,当最上层的goroutine都不在计算了,优雅通知

衍生出来的goroutine可以进行优雅停机,一般情况下可以在子routine中检测到停止信号,进行抛异常或退出操作。

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值