目 录CONTENT

文章目录

Go并发编程-Context上下文

Sakura
2023-09-18 / 0 评论 / 1 点赞 / 43 阅读 / 15695 字 / 正在检测是否收录...

一: Context 基本介绍

Go 1.7 标准库引入context,译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如database/sql包。context 几乎成为了并发控制和超时控制的标准做法

作用:

在一组 goroutine 之间传递共享的值、取消信号、deadline

  • 在 HTTPServer 中 :

Context 2 中 , 三个 goroutine 传递的是相同的请求

二: Context 核心结构

context.Context是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法:

type Context interface {
    // 返回被取消的时间
    Deadline() (deadline time.Time, ok bool)
    // 返回用于通知Context完结的channel
    // 当这个 channel 被关闭时,说明 context 被取消了
    // 在子协程里读这个 channel,除非被关闭,否则读不出来任何东西
    Done() <-chan struct{}
    // 返回Context取消的错误
    Err() error
    // 返回key对应的value
    Value(key any) any
}

Done() : 会关闭传递信号的 channel , 当 channel 被关闭了 , 就可以接收到数据了 ( context 被取消了 )

value() : 通过key:value 来传递context中需要共享的值

  • 除了Context接口,还存在一个 canceler 接口,用于实现 Context 可以被取消

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

可以取消的上下文都应该实现 canceler 接口

  • 除了以上两个接口,还有4个预定义的Context类型:

// 空Context
type emptyCtx int

// 取消Context
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
}

// 定时取消Context
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

// KV值Context
type valueCtx struct {
    Context
    key, val any
}

三: 默认 Context 使用

context 包中最常用的方法是context.Backgroundcontext.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:

  • context.Background是上下文的默认值,所有其他的上下文都应该从它衍生出来,在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。

  • context.TODO,是一个备用,一个 context 占位,通常用在并不知道传递什么 context的情形。

底层都是返回一个空context

// 创建方法
func Background() Context {
    return background
}
func TODO() Context {
    return todo
}

// 预定义变量
var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

// emptyCtx 定义
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key any) any {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

作用:

有些函数或者方法需要 context , 但是自己没有 context , 这时候就可以定义空 context

  • database/sql包中的某些函数

func (db *DB) PingContext(ctx context.Context) error
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
  • 具体使用

db, _ := sql.Open("", "")
query := "DELETE FROM `table_name` WHERE `id` = ?"
db.ExecContext(context.Background(), query, 42)

四: Contex 主动传递取消信号

1. 主动取消

需要的操作为:

  1. 创建带有cancel函数的Context,func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  2. 接收 cancel 的Channel,ctx.Done()

  3. 主动 Cancel 的函数,cancel CancelFunc

具有同一个 context 对象的 goroutine , 调用主动 Cancel 取消函数后 , ctx.Done()就可以取出数据

每一个 goroutine 都会监听c.Done() 这个 channel , 当监听到之后就会执行用户自己定义的操作 , 比如return 退出所有goroutine

取消这个操作不是由 context 执行的 , context 只负责去传递这个信号 , 有了这个信号 , 就可以基于这个信号做后续操作

func ContextCancel() {
	// 一:创建带有cancel函数的context
	ctx, cancel := context.WithCancel(context.Background())

	// 二: 启动goroutine,携带cancelCtx
	wg := sync.WaitGroup{}
	wg.Add(4)
	for i := 0; i < 4; i++ {
		go func(c context.Context, n int) {
			defer wg.Done()
			// 监听context的取消完成channel,来确定是否执行主动cancel操作
			fmt.Println("第", n, "个Goroutine")
			for {
				select {
				// 等待接收c.Done()这个channel
				case <-c.Done():
					fmt.Println("第", n, "个 Goroutine ", "context cancel")
					return
				default:

				}
				time.Sleep(300 * time.Millisecond)
			}
		}(ctx, i)
	}

	// 三: 定时取消cancel()
	// 定时器,三秒后取消所有goroutine的执行
	select {
	case <-time.NewTimer(3 * time.Second).C:
		fmt.Println("3秒时间到")
		cancel()
	}
	// 也可以使用select解决goroutine结束无法打印contex cancel问题
	select {
	case <-ctx.Done():
		fmt.Println("main context cancel")
	}
	wg.Wait()
}

2. Deadline 和 Timeout 定时取消

  • context.WithTimeout() 某个时间段 , 比如5秒后

  • context.WithDeadline() 某个时间点 . 比如每天20:30

// 10s后cancel
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

// 20:30 cancel
// time.Data()的方式
curr := time.Now()
t := time.Date(curr.Year(), curr.Month(), curr.Day(), 20, 30, 0, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), t)

// 以当前的时间加10分钟
context.WithDeadline(context.Background(),time.Now().Add( 10 * time.Minute))

带有时间的自动取消也可以自行调用Cancel() 来实现主动取消

select {
	// 3秒后主动取消
	case <-time.NewTimer(3 * time.Second).C:
		cancel()
	// 通过withTimeout自动取消
	// context.WithTimeout(context.Background(), 10 * time.Second) 
	case ctx.Done() 
}

使用场景 : 不能确定主动调用是否能够调用成功 , 就可以使用但是取消

  • 从底层开始看出 , 定时取消是在主动取消的基础上增加的功能

type timerCtx struct {
	cancelCtx // 主动取消
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}
  • WithTimeout() 也是利用WithDeadline() 实现的

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

3. Cancel 操作的向下传递

当父上下文被取消时 , 子上下文也会被取消

func ContextExtends() {
	// 定义符合上图的context结构
	ContextOne, _ := context.WithCancel(context.Background())
	ContextTwo, cancel := context.WithCancel(ContextOne)
	ContextThree, _ := context.WithCancel(ContextOne)
	ContextFour, _ := context.WithCancel(ContextTwo)

	wg := sync.WaitGroup{}
	wg.Add(4)
	// 开启四个goroutine分别监控四个context的Done()方法
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextOne.Done():
			fmt.Println("contextOne cancel")
		}
	}(ContextOne)
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextTwo.Done():
			fmt.Println("contextTwo cancel")
		}
	}(ContextTwo)
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextThree.Done():
			fmt.Println("contextThree cancel")
		}
	}(ContextThree)
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextFour.Done():
			fmt.Println("contextFour cancel")
		}
	}(ContextFour)

	// 手动取消信号
	cancel()
	wg.Wait()
}

通过给对应 Context 执行 cancel , 输出遵循: 当父上下文被取消时 , 子上下文也会被取消

五: 取消操作流程

1. 创建 cancelCtx 的流程

2. ctx.Done() 初始信号 channel 流程

3. cancel() 操作流程

六: Context 传值

如果在 ContextB 中设置了一个值 , 那么这个值只会在 contextA 和基于A产生的 contextC 和 contextD 中使用

Web 开发中 , 每一个新请求创建一个 context 中 , 将数据存储到 context 中 , 当前 context 中的后续调用都可以用到该值

context 数据类型 : key - value 数据

func WithValue(parent Context, key, val any) Context

type Context interface {
    Value(key any) any
}

需要三个参数:

  • 上级 Context

  • key 要求是 comparable 的(可比较的),实操时,推荐使用特定的 Key 类型,避免直接使用 string 或其他内置类型而带来 package 之间的冲突。

// 避免冲突
// 例如其他包中有相同的字段"name"
type MyContextKey String
  • val any

1. 单个 context 传值

type MyContext string
func ContextValue() {
	wg := sync.WaitGroup{}
	// 1.创建带有value的context
	ctx := context.WithValue(context.Background(), MyContextKey("name"), "Sakura")

	// 2.将ctx传入goroutine中
	wg.Add(1)
	key := "name"
	go func(c context.Context, key any) {
		defer wg.Done()
		// 通过key拿到value
		if value := c.Value(key); value != nil {
			fmt.Println("value:", value)
			return
		}
		fmt.Println("找不到key为\"", key, "\"的数据")

	}(ctx, key)

	wg.Wait()
}

要注意获取到的 value 为 any 空接口类型 , 如果想要使用 value 需要进行断言或者类型判断

  • 查看WithValue()可以返回返回值为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}
}
type valueCtx struct {
	Context
	key, val any
}

// valueCtx实现了这个value这个方法
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

也就是除了 value 功能,其他 Contenxt 功能都由parent Context实现。

如果 context.valueCtx.Value 方法查询的 key 不存在于当前 valueCtx 中,就会从父上下文中查找该键对应的值直到某个父上下文中返回

2. 多个context传值

type MyContext string
func ContextValue() {
	wg := sync.WaitGroup{}
	// 1.创建带有value的context
	ctxOne := context.WithValue(context.Background(), MyContext("name"), "Sakura1")
	ctxTwo := context.WithValue(ctxOne, MyContext("name"), "Sakura2")
	ctxThree := context.WithValue(ctxTwo, MyContext("name"), "Sakura3")

	// 2.将ctx传入goroutine中
	wg.Add(1)
	key := "name"
	go func(c context.Context, key any) {
		defer wg.Done()
		// 通过key拿到value
		if value := c.Value(key); value != nil {
			fmt.Println("value:", value)
			return
		}
		fmt.Println("找不到key为\"", key, "\"的数据")

	}(ctxThree, key)

	wg.Wait()
}

不存在会返回该 context 中上一级的 value

七 : 使用 context 的注意事项

  • 推荐以参数的方式显示传递Context

  • 以Context作为参数的函数方法,应该把Context作为第一个参数。

  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()

  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数

  • Context是线程安全的,可以放心的在多个goroutine中传递

1

评论区