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