go context 包

作者: 哥斯拉啊啊啊哦 | 来源:发表于2019-08-23 21:04 被阅读0次

Go服务器中,每个传入的请求都在其自己的goroutine中处理。
请求处理程序通常会启动其他goroutine来访问后端,例如数据库和RPC服务
处理请求的goroutine集合通常需要访问特定于请求的值,例如最终用户的身份,授权令牌和请求的截止日期。
当请求被取消或超时时,处理该请求的所有goroutine都应该快速退出,以便系统可以回收它们正在使用的任何资源。
go提供了一个'context'包,也就是上下文,来达到控制他们的目的,可以轻松地将请求范围的值,取消信号和API边界的截止日期传递给处理请求所涉及的所有goroutine


context的使用

在请求处理的过程中,会调用各层的函数,每层的函数会创建自己的routine,构成了一个routine树, 如果由Context来控制上下文,context也应该反映并实现成一棵树


根节点
要创建context树,第一步是要有一个根结点。context.Background函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间

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

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
    return background
}

  • 子节点
    context包为我们提供了以下函数,当一个context取消,所有从他衍生的contexts都会被取消

  • context的带的方法

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline: 返回1个time.time,表示当前context应该结束的时间,ok表示有结束时间
  • Done: context被取消或超时返回close的chan,告诉context相关的函数停止当前工作返回(类似全局广播)
  • Err: context被取消的原因
  • Value: context实现共享数据的地方,是协程安全的

Context 所以方法
func Background() Context
func TODO() Context

// ctx, cancel := context.WithCancel(Background())
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(5 * time.Second)) 5秒之后取消
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

// ctx, cancel := context.WithTimeout(ctx, 10*time.Second)  10秒后取消
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

// ctxVal := context.WithValue(ctx, "hello", "world") 
func WithValue(parent Context, key interface{}, val interface{}) Context

这四个函数的第一个参数都是父context,返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收的函数参数保存子节点的一些状态值,然后就可以将它传递给下层的routine了

例子
  • 例一:WithCancel()
func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // 用chan实现锁
    c := make(chan bool, 1)

    go worker(ctx, "worker1", c)
    go worker(ctx, "worker2", c)
    go worker(ctx, "worker3", c)

    time.Sleep(5 * time.Second)
    fmt.Println("通知工作停止")
    cancel()
    for i := 0; i < 3; i++ {
        // 接收到3次数据,表明3个worker都关闭
        // 如果没有这个锁,可能cancel()执行后,协程可能没来得及退出,主函数就结束了
        // 比sync.waitGroup好用,wait跟context搭配容易出奇奇怪怪的问题
        <-c
    }
    close(c)
    fmt.Println("所有工作停止")
}

func worker(ctx context.Context, workerId string, c chan bool) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(workerId, "goroutine stop worked")
            c <- true
            return
        default:
            fmt.Println(workerId, "goroutine is working...")
            time.Sleep(1500 * time.Millisecond)
        }
    }
}

  • 例二:WithDeadline
func main() {
    // 截至某某时间,自动取消,也可以用cancel提前取消
    // 当前时间的5秒后,自动取消
    t := time.Now().Add(5 * time.Second)
    ctx, _ := context.WithDeadline(context.Background(), t)

    c := make(chan bool, 1)

    go worker(ctx, "worker1", c)
    go worker(ctx, "worker2", c)
    go worker(ctx, "worker3", c)

    for i := 0; i < 3; i++ {
        <-c
    }
    close(c)
    fmt.Println("所有工作停止")
}

func worker(ctx context.Context, workerId string, c chan bool) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(workerId, "goroutine stop worked")
            c <- true
            return
        default:
            fmt.Println(workerId, "goroutine is working...")
            time.Sleep(1500 * time.Millisecond)
        }
    }
}

  • 例三:WithTimeout
func main() {
    // cancel可以忽略,5秒后自动取消
    ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)

    c := make(chan bool, 1)

    go worker(ctx, "worker1", c)
    go worker(ctx, "worker2", c)
    go worker(ctx, "worker3", c)

    for i := 0; i < 3; i++ {
        <-c
    }
    close(c)
    fmt.Println("所有工作停止")
}

func worker(ctx context.Context, workerId string, c chan bool) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(workerId, "goroutine stop worked")
            c <- true
            return
        default:
            fmt.Println(workerId, "goroutine is working...")
            time.Sleep(1500 * time.Millisecond)
        }
    }
}

  • 例四:WithValue
func main() {
    ctx, cancel := context.WithCancel(context.Background())

    val := "hello world"
    // 由于withValue没有返回cancel,只能作为有cancel节点的子节点
    ctxVal := context.WithValue(ctx, "worker", val)

    c := make(chan bool, 1)

    go worker(ctxVal, c)
    go worker(ctxVal, c)
    go worker(ctxVal, c)

    time.Sleep(5 * time.Second)

    fmt.Println("通知所有工作准备停止")
    cancel()
    for i := 0; i < 3; i++ {
        <-c
    }
    close(c)
    fmt.Println("所有工作已经停止")
}

func worker(ctx context.Context, c chan bool) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Value("worker"), "goroutine stop worked")
            c <- true
            return
        default:
            fmt.Println(ctx.Value("worker"), "goroutine is working...")
            time.Sleep(1500 * time.Millisecond)
        }
    }
}
总结

context包是用来管理进程以及衍生出的协程。打一个比方,一个项目三组人,项目相当于进程,每一组相对于一个线程,组里面每个人相当于协程。
当一个组任务完成了,组员们可以停下你们的工作了。那么现实中就是需要,组长告知大家,所有人停止任务。同样协程也需要,一个信号告诉他们停止任务。
context包就起到通知的功能。项目进程持有一个顶级A Context,每个组持有A Context衍生的B,C,D Context,组成员持有组衍生X,Y,Z Context.

Context 使用原则

不要把Context放在结构体中,要以参数的方式传递
以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
Context是线程安全的,可以放心的在多个goroutine中传递

相关文章

网友评论

    本文标题:go context 包

    本文链接:https://www.haomeiwen.com/subject/oxutectx.html