Golang 带有取消功能的Context

485 阅读3分钟

本文基于golang 1.17对Golang 带有取消功能的Context的实现进行学习,了解其实现取消操作的实现。

开始之前我们先上一段简单的代码来看看效果。

package main

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

func main() {
	ctx := context.TODO()
	cancel, cancelFunc := context.WithCancel(ctx)
	go func(ctx context.Context) {
		time.Sleep(time.Second * 5)
		for {
			select {
			case a := <-ctx.Done():
				fmt.Println(ctx.Err())
				fmt.Println("end", a)
				return
			default:
				fmt.Println("hello")
			}
		}
	}(cancel)
	cancelFunc()
	a := make(chan struct{})
	<-a

	v := make(chan struct{})
	b := make(<-chan struct{})
	if v == b {
		fmt.Println("hello")

	}
}

这段代码做了以下几件事情

  1. 创建一个没有具体实现的context context.TODO()函数返回没有具体实现的context,第10行
  2. 创建出带有取消功能的context,第11行
  3. 创建一个goroutines,并监听context是否被取消,第12~21 行
  4. 取消context,第22行
  5. 休眠3秒,第23行

最后的输出应该是:

context canceled

end

接下来我们来看看这个功能是怎么实现的。通过的上面的代码我们知道取消context这个动作是由函数cancelFunc()触发的,进入context.WithCancel()看看这个函数的具体实现

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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) }
}

函数将传入的context作为父级创建了一个新的子级context,然后返回新创建的context和一个CancelFun类型的函数,通过最开始的演示代码知道就是这个函数出发了取消的context的动作。在查看取消函数之前我们先看看新创建出的context的结构是什么

知道了cancelCtl的包含哪些字段之后进一步看看是如何取消context的

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of 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
}

这是具体的取消函数,这个函数的主要作用就是向done中放入一个空的结构体,关闭chan d 第17行,然后依次取消当前context的子context,

接下来我们来看看context是怎么通过Done()得到的取消通知

func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{})
		c.done.Store(d)
	}
	return d.(chan struct{})
}

通过阅读代码这个函数只是到done中的取值,如果没有就创建一个chan struct{},并加载到done。所有chan struct的作用只是一个占位的作用,当done中的chan 被关闭的的时候我们自然就可以知道context已经被取消。

在取消函数里我们看见

d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}

这段代码,closedchan 是一个已经关闭的chan

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

func init() {
	close(closedchan)
}

所以如果在执行取消函数的时候done里没有值的话就直接加载一个已经关闭的chan。

带有取消功能context可以让我们控制创建的goroutines结束自己。

欢迎指正文章中的错误