美文网首页Go进阶系列
10 Go Context 上下文

10 Go Context 上下文

作者: GoFuncChan | 来源:发表于2019-07-10 00:31 被阅读0次

    一、Context概述

    1.缘起

    在开发web服务应用时,我们知道http启动的服务每接收到一个请求是便启动一个goroutine处理该request。而每个协程处理该请求是一般都会启动多个协程去处理不同任务,如调用RPC、访问数据库资源、缓存资源等等,这些协程都是为处理同一个request工作的,同时当request被取消或者超时的时候,从这个request处理协程创建的所有子协程也应该被结束。此时一个handler就必须对其启动的子协程有控制权,在context出现前,上述那些处理还是很丑陋的,有些甚至引起全局资源的滥用或者回调噩梦。context出现后,一切都得到解脱,context解决了处理同一生命周期协程树的资源管理问题。

    2.官方解释:

    Context,翻译为“上下文”,context包定义了Context接口类型,其接口签名方法定义了跨API边界和进程之间的执行最后期限、取消信号和其他请求范围的值。

    对服务器的传入请求应创建Context类型,对服务器的传出调用应接受Context。它们之间的函数调用链必须传播Context,可以选择将其替换为使用WithCancel()、WithDeadline()、WithTimeout()或WithValue()创建的派生Context。当一个context被取消时,从它派生的所有context也被取消。WithCancel()、WithDeadline()和WithTimeout()函数接受上下文(父级)并返回派生上下文(子级)和Cancelfunc。调用Cancelfunc将取消子级及其子孙级,删除父级对子级的引用,并停止任何关联的计时器。如果不调用Cancelfunc,则会泄漏子级及其子孙级,直到父级被取消或计时器触发。Go-Vet工具检查取消功能是否用于所有控制流路径。

    使用Context的程序应该遵循这些规则,以保持包之间的接口一致,并允许静态分析工具检查上下文传播:

    //context传递的写法
    func DoSomething(ctx context.Context, arg Arg) error {
        // ... use ctx ...
    }
    
    • 不要将Context存储在结构类型中;而是将Context显式传递给每个需要它的函数。文应该是第一个参数,通常名为ctx:

    • 即使函数允许,也不要传递nil上下文。如果不确定要使用哪个上下文,请传递context.TODO(),该函数返回一个可被跟踪的顶级Context。

    • 只对传输进程和API的请求范围数据使用Context值,而不用于向函数传递可选参数。

    • 同一Context可以传递给在不同goroutine中运行的函数;上下文对于多个goroutine同时使用是安全的。

    3.context包解析

    我们来看一下Context接口的签名方法:

    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
    3.1 Context接口签名方法解析:
    • Deadline() (deadline time.Time, ok bool)

    Deadline方法返回应取消代表此上下文完成的工作的时间。

    • 未设置截止时间时,Deadline方法返回ok==false。
    • 对Deadline方法的连续调用将返回相同的结果。
    • Done() <-chan struct{}

    done返回一个通道,该通道在应取消代表此上下文完成的工作时关闭。如果无法取消此上下文,则done可能返回nil。对done的连续调用返回相同的值。不同的派生Context对done通道关闭有不同的处理方式:

    • WithCancel()在调用cancel时安排关闭done;
    • WithDeadline()在截止时间过期时安排关闭done;
    • WithTimeout()在超时结束时安排关闭done。
    • Err() error

    Err方法在done关闭后返回非零错误值。
    返回值:

    • 如果上下文被取消,则返回Canceled;如果上下文的截止时间已过,则返回DeadLineExceeded;
    • 没有为err定义其他值。
    • 完成后关闭,对err的连续调用将返回相同的值。
    • Value(key interface{}) interface{}
    • 该方法可以让协程共享一些数据,获得数据是协程安全的。
    • 该方法返回与键的上下文关联的值,如果没有值与键关联,则返回nil。
    • 对具有相同键的值的连续调用返回相同的结果。
      仅对传输进程和API边界的请求范围数据使用上下文值,而不用于向函数传递可选参数。
      键标识上下文中的特定值。希望在上下文中存储值的函数通常在全局变量中分配一个键,然后使用该键作为context.WithValue() 和 Context.Value的参数。键可以是支持相等的任何类型;包应将键定义为未排序的类型以避免冲突。
    3.2 顶级Context

    context包提供两种顶级的上下文类型,由工厂方法创建:

    (1).func Background() Context

    context.Background()返回非零的空上下文。它从不被取消,没有值,也没有最后期限。它通常由主函数、初始化和测试使用,并且作为传入请求的顶级上下文。

    (2).func TODO() Context

    context.TODO()返回非零的空上下文。当不清楚要使用哪个上下文或者它还不可用时(因为周围的函数还没有被扩展以接受上下文参数),应该使用context.TODO()。静态分析工具可以识别TODO,它确定上下文是否在程序中正确传播。

    两者区别:

    ==本质来讲两者区别不大,其源码实现是一样的,只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播。==

    3.3 派生Context

    除以上两种顶级Context类型,context包提供四种创建可派生Context类型的函数:

    (1). func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

    WithCancel函数返回具有新done通道的父级副本。当调用返回的cancel函数或关闭父上下文的done通道时(以先发生者为准),将关闭返回的上下文的done通道。
    取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。

    官方使用示例:

    // gen generates integers in a separate goroutine and
    // sends them to the returned channel.
    // The callers of gen need to cancel the context once
    // they are done consuming generated integers not to leak
    // the internal goroutine started by gen.
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // returning not to leak the goroutine
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }
    
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // cancel when we are finished consuming integers
    
    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
    
    //OUTPUT:
    1
    2
    3
    4
    5
    
    
    (2). func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

    WithDeadline函数返回父上下文的副本,其截止时间调整为不迟于d。如果父上下文的截止时间早于d,则WithDeadline(Parent,d)在语义上等同于父上下文。当截止时间到期、调用返回的cancel函数或关闭父上下文的done通道(以先发生者为准)时,返回的上下文的done通道将关闭。
    取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。

    官方使用示例:
    这个例子传递一个具有任意截止时间的上下文,告诉一个阻塞函数一旦到达它就应该放弃它的工作。

    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)
    
    // Even though ctx will be expired, it is good practice to call its
    // cancelation function in any case. Failure to do so may keep the
    // context and its parent alive longer than necessary.
    defer cancel()
    
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
    
    //OUTPUT:
    context deadline exceeded
    
    (3). func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

    WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消:

    官方使用示例:这个例子传递一个带有超时的上下文,告诉一个阻塞函数它应该在超时结束后放弃它的工作。

    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
    
    
    //OUTPUT:
    context deadline exceeded
    
    
    *以上函数的特殊返回类型值:type CancelFunc func()

    CancelFunc告诉操作放弃其工作。CancelFunc不等待工作停止。在第一次调用之后,对CancelFunc的后续调用不做任何操作。

    (4). func WithValue(parent Context, key, val interface{}) Context

    WithValue返回父级的副本,可为上下文设置一个键值对。
    只对传输进程和API的请求范围数据使用上下文值,而不用于向函数传递可选参数。
    提供的键必须是可比较的,并且不应是字符串或任何其他内置类型,以避免使用上下文的包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给接口时进行分配,上下文键通常具有具体的类型结构。或者,导出的上下文键变量的静态类型应该是指针或接口。

    官方使用示例:

    type favContextKey string
    
    f := func(ctx context.Context, k favContextKey) {
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }
    
    k := favContextKey("language")
    ctx := context.WithValue(context.Background(), k, "Go")
    
    f(ctx, k)
    f(ctx, favContextKey("color"))
    
    
    //OUTPUT:
    found value: Go
    key not found: color
    

    三、Context使用示例

    使用context包来实现线程安全退出或超时的控制:

    
    //定义一个并发worker
    func worker(ctx context.Context, wg *sync.WaitGroup) error {
        defer wg.Done()
    
        for {
            select {
            //当父协程调用cancel()时,会从ctx.Done()得到struct{},此时返回ctx.Err()退出子线程
            case <-ctx.Done():
                return ctx.Err()
            default:
            //默认输出hello
            fmt.Println("hello")
            }
        }
    }
    
    func main() {
        //生成一个有超时控制的衍生Context,超时10s退出所有子协程
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go worker(ctx, &wg)
        }
        
        //主协程1s后就cancel所有子协程了,每个worker都可以安全退出
        time.Sleep(time.Second)
        cancel()
        wg.Wait()
    }
    

    当并发体超时或main主动停止工作者Goroutine时,每个工作者都可以安全退出。

    Go语言是带内存自动回收特性的,因此内存一般不会泄漏。当main函数不再使用管道时后台Goroutine有泄漏的风险。我们可以通过context包来避免这个问题,下面是防止内存泄露的素数筛实现:

    
    // 返回生成自然数序列的管道: 2, 3, 4, ...
    func GenerateNatural(ctx context.Context) chan int {
        ch := make(chan int)
        go func() {
            for i := 2; ; i++ {
                select {
                //父协程cancel()时安全退出该子协程
                case <- ctx.Done():
                    return
                //生成的素数发送到管道
                case ch <- i:
                }
            }
        }()
        return ch
    }
    
    // 管道过滤器: 删除能被素数整除的数
    func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
        out := make(chan int)
        go func() {
            for {
                if i := <-in; i%prime != 0 {
                    select {
                    //父协程cancel()时安全退出该子协程
                    case <- ctx.Done():
                        return
                    case out <- i:
                    }
                }
            }
        }()
        return out
    }
    
    func main() {
        // 使用一个可由父协程控制子协程安全退出的Context。
        ctx, cancel := context.WithCancel(context.Background())
    
        ch := GenerateNatural(ctx) // 自然数序列: 2, 3, 4, ...
        
        for i := 0; i < 100; i++ {
            // 新出现的素数打印出来
            prime := <-ch 
            fmt.Printf("%v: %v\n", i+1, prime)
            // 基于新素数构造的过滤器
            ch = PrimeFilter(ctx, ch, prime) 
        }
        
        //输出100以内符合要求的素数后安全退出所有子协程
        cancel()
    }
    

    当main函数完成工作前,通过调用cancel()来通知后台Goroutine退出,这样就避免了Goroutine的泄漏。

    相关文章

      网友评论

        本文标题:10 Go Context 上下文

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