Go语言context包的简单介绍

简介

  • 在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
  • 在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。现在context已成为官方库,使用的时候只需要import "context"即可。

Context基本数据结构

【Context interface】

Context interface是最基本的接口:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline()返回一个time.Time,是当前 Context 的应该结束的时间,ok 表示是否有 deadline
  • Done()返回一个 channel,这个 channel 对于以 Context 方式运行的函数而言,是一个取消信号。当这个 channel 关闭时,上面提到的这些函数应该终止手头的工作并立即返回。
  • Err()返回 Context 被取消时的错误
  • Value(key interface{}) 允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。
【canceler interface】

canceler interface 定义了提供 cancel 函数的 context,要求数据结构要同时实现 Context interface。

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

除了以上两个 interface 之外,context 包中还定义了若干个struct,来实现上面的 interface

  • emptyCtx
    emptyCtx是空的Context,只实现了Context interface,只能作为 root context 使用。
type emptyCtx int
  • cancelCtx
    cancelCtx继承了Context并实现了cancelerinterface,从WithCancel()函数产生.
type cancelCtx struct {
    Context

    done chan struct{} // closed by the first cancel call.

    mu       sync.Mutex
    children map[canceler]bool // set to nil by the first cancel call
    err      error             // set to non-nil by the first cancel call
}
  • timerCtx
    timerCtx继承了cancelCtx,所以也自然实现了Context和canceler这两个interface,由WithDeadline()函数产生。·
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

deadline time.Time
}
  • valueCtx
    valueCtx包含key、val field,可以储存一对键值对,由WithValue()函数产生。
type valueCtx struct {
    Context
    key, val interface{}
}

Context实例化和派生

【Background()函数】

Context 只定义了 interface,真正使用时需要实例化,官方首先定义了一个 emptyCtx struct 来实现 Context interface,然后提供了Backgroud()函数来便利的生成一个 emptyCtx 实例。实现代码如下:

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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 interface{}) interface{} {
    return nil
}

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

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

Backgroud() 生成的 emptyCtx 实例是不能取消的,因为emptyCtx没有实现canceler interface,要正常取消功能的话,还需要对 emptyCtx 实例进行派生。常见的两种派生用法是WithCancel() 和 WithTimeout。

【WithCancel()函数】

调用WithCancel()可以将基础的 Context 进行继承,返回一个cancelCtx示例,并返回一个函数,可以在外层直接调用cancelCtx.cancel()来取消 Context。代码如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{
        Context: parent,
        done:    make(chan struct{}),
    }
}
【WithTimeout()函数】

调用WithTimeout,需要传一个超时时间。来指定过多长时间后超时结束 Context,源代码中可以得知WithTimeout是WithDeadline的一层皮,WithDeadline传的是具体的结束时间点,这个在工程中并不实用,WithTimeout会根据运行时的时间做转换。代码如下:

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

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  deadline,
    }
    propagateCancel(parent, c)
    d := deadline.Sub(time.Now())
    if d <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(true, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(d, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

在WithDeadline中,将 timeCtx.timer 挂上结束时的回调函数,回调函数的内容是调用cancel来结束 Context。

【WithValue()函数】

创建一个存储 k-v 对的 context。代码如下:

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

Context控制多个goroutine示例

以下代码启动了 3 个监控 goroutine 进行不断的运行任务,每一个都使用了 Context 进行跟踪,当我们使用 cancel 函数通知取消时,这 3 个 goroutine 都会被结束。这就是 Context 的控制能力,它就像一个控制器一样,按下开关后,所有基于这个 Context 或者衍生的子 Context 都会收到通知,这时就可以进行清理操作了,最终释放 goroutine,这就解决了 goroutine 启动后不可控的问题。

package main

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

// 使用context控制多个goroutine
func watch(ctx context.Context, name string) {
    for {
        select {
        case <- ctx.Done():
            fmt.Println(name, "退出 ,停止了。。。")
            return
        default:
            fmt.Println(name, "运行中。。。")
            time.Sleep(2 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx, "【任务1】")
    go watch(ctx, "【任务2】")
    go watch(ctx, "【任务3】")

    time.Sleep(time.Second * 10)
    fmt.Println("通知任务停止。。。。")
    cancel()
    time.Sleep(time.Second * 5)
    fmt.Println("真的停止了。。。")
}

运行结果如下:
在这里插入图片描述

参考博客

https://studygolang.com/articles/9624
https://www.cnblogs.com/qcrao-2018/archive/2019/06/12/11007503.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值