美文网首页程序员
Golang问题点(三) - Context的问题点

Golang问题点(三) - Context的问题点

作者: 董小贱 | 来源:发表于2020-05-22 12:44 被阅读0次

    Go的标准库 context

    在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。

    那么如何控制goroutine优雅的退出呢?

    1. 实例

    1.0. 基础代码

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var wg sync.WaitGroup
    
    func DoSomething() {
        for {
            fmt.Println("董小贱")
            time.Sleep(2 * time.Second)
        }
        wg.Done()
    }
    
    func main() {
        wg.Add(1)
      go DoSomething()  //   这个DoSomething() 没有停止条件,不会停止
        wg.Wait()
        fmt.Println("DoSomething over")
    }
    

    1. 1全局变量方式

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var wg sync.WaitGroup
    var status bool = true
    
    func DoSomething() {
        for {
            fmt.Println("董小贱")
            time.Sleep(2 * time.Second)
            if !status {
                break
            } else {
                continue
            }
        }
        wg.Done()
    }
    
    func main() {
        wg.Add(1)
        go DoSomething()
        time.Sleep(10 * time.Second)
        status = false  //通过修改全局变量的状态,给gorotine发送终止信号
        wg.Wait()
        fmt.Println("DoSomething over")
    }
    

    1.2. select + channel 版本

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var wg sync.WaitGroup
    var chann = make(chan bool)
    
    func DoSomething() {
    LoopTag:
        for {
            fmt.Println("董小贱")
            time.Sleep(2 * time.Second)
            select {
            case <-chann: // 信号接收
                break LoopTag // 退出整个lable
            default:
                continue
            }
        }
        wg.Done()
    }
    
    func main() {
        wg.Add(1)
        go DoSomething()
        time.Sleep(10 * time.Second)
        chann <- false // 通过无缓存的channel来给goroutine发送终止信号
        wg.Wait()
        fmt.Println("DoSomething over")
    }
    
    

    1.3.context版本

    package main
    
    import (
        "context"
        "fmt"
        "sync"
        "time"
    )
    
    var wg sync.WaitGroup
    
    func DoSomething(ctx context.Context) {
    LoopTag:
        for {
            fmt.Println("董小贱")
            time.Sleep(2 * time.Second)
            select {
            case <-ctx.Done(): // 等待父级通知
                break LoopTag
            default:
                continue
            }
        }
        wg.Done()
    
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        wg.Add(1)
        go DoSomething(ctx)
        time.Sleep(10 * time.Second)
        cancel() //cancel()执行,通知子goroutine结束
        wg.Wait()
        fmt.Println("DoSomething over")
    }
    
    

    当在DoSomething中再开启子goroutine时,将ctx传入到函数当中即可:

    package main
    
    import (
        "context"
        "fmt"
        "sync"
        "time"
    )
    
    var wg sync.WaitGroup
    
    func DoSomething2(ctx context.Context) {
    LoopTag:
        for {
            fmt.Println("董小小贱")
            time.Sleep(2 * time.Second)
            select {
            case <-ctx.Done():
                break LoopTag
            default:
                continue
            }
        }
        wg.Done()
    }
    
    func DoSomething(ctx context.Context) {
        go DoSomething2(ctx)
    LoopTag:
        for {
            fmt.Println("董小贱")
            time.Sleep(2 * time.Second)
            select {
            case <-ctx.Done():
                break LoopTag
            default:
                continue
            }
        }
        wg.Done()
    
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        wg.Add(1)
        go DoSomething(ctx)
        time.Sleep(10 * time.Second)
        cancel()
        wg.Wait()
        fmt.Println("DoSomething over")
    }
    

    那么context的作用就呼之欲出了:简单讲就是用来处理goroutine之间传递截止日期、取消信号和其他请求范围内的值

    2. context相关


    Go1.7 加入的新的标准库context 它定义了Context类型, 它跨API边界和进程之间传递截止日期、取消信号和其他请求范围内的值。

    对服务器的传入请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传播上下文,可以选择将其替换为使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,从它派生的所有上下文也被取消。

    WithCancelWithDeadlineWithTimeout函数接收一个Context并返回一个派生子Context和一个CancelFunc。调用CancelFunc取消子进程及其子进程,删除父进程对子进程的引用,并停止所有相关的计时器。如果没有调用CancelFunc,则会泄漏子节点及其子节点,直到父节点被取消或计时器触发。

    不要将Context存储在结构类型中,应该显示地将上下文传递给需要它的每个函数,Context应该是第一个参数,通常命名为ctx。


    2.1 context包相关

    2.1.1 context.Context是一个接口,该接口定义了四个需要实现的方法。具体签名如下:
    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
    
    // Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);
    //Done方法需要返回一个Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done方法会返回同一个Channel;
    //Err方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值;
            //如果当前Context被取消就会返回Canceled错误;
            //如果当前Context超时就会返回DeadlineExceeded错误;
    //Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
    
    2.1.2 CancelFunc
    type CancelFunc func()
    
    // CancelFunc告诉一个goroutine放弃它的工作。CancelFunc不会等待工作停止。在第一次调用之后,对CancelFunc的后续调用什么也不做。
    
    2.1.3 Background()
    func Background() Context
    
    // Background返回一个非nil的空上下文。它从未被取消,没有值,也没有期限。它通常由主函数、初始化和测试使用,并作为传入请求的根context(最顶层的context,其他的都是它的派生子context)
    
    2.1.4 TODO()
    func TODO() Context
    
    //TODO返回一个非nil的空context。代码应该使用context。TODO当不清楚要使用哪个context或者它还不可用时(因为周围的函数还没有被扩展到接受context参数)。TODO由静态分析工具识别,该工具确定context是否在程序中正确传播
    
    //这个就是知道要用context,但不清楚用哪个,埋点用。
    
    backgroundtodo本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

    2.2 With系列函数

    2.2.1 WithCancel
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    
    //WithCancel返回带有新Done通道的父进程的一个副本。当返回的cancel函数被调用时,或者当父context的Done通道被关闭时,返回context的Done通道将被关闭,以最先发生的情况为准。
    
    // 取消此context释放与之关联的资源,因此代码应该在此context中运行的操作完成后立即调用cancel。
    

    具体用法可以看上边的例子。

    2.2.2 WithDeadline
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    
    // deadline 是绝对时间,比如:d := time.Now().Add(50 * time.Millisecond)
    
    //WithDeadline返回父context的一个副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。返回的上下文的Done通道在deadline过期时关闭,在调用返回的cancel函数时关闭,或者在关闭父上下文的Done通道时关闭,以最先发生的情况为准。
    
    // 取消此上下文释放与之关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
    

    一个例子:

    func main() {
        d := time.Now().Add(50 * time.Millisecond)
        ctx, cancel := context.WithDeadline(context.Background(), d)
        defer cancel()
        select {
        case <-time.After(1 * time.Second):
            fmt.Println("overslept")
        case <-ctx.Done():
            fmt.Println(ctx.Err())
        }
    }
    
    
    // 这里会先执行ctx.Done() 然后打印异常
    
    2.2.1 WithTimeout
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    
    // 跟WithDeadline的区别在于,WithTimeout是用的相对时间。
    
    2.2.1 WithValue
    func WithValue(parent Context, key, val interface{}) Context
    
    //WithValue返回父节点的一个副本,其中与键关联的值为val。
    
    //只将上下文值用于传输流程和api的请求范围的数据,而不用于将可选参数传递给函数。
    
    // 提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用context的包之间的冲突。WithValue的用户应该定义自己的键类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。
    
    

    一个例子:

    package main
    
    import (
        "context"
        "fmt"
    )
    
    type favContextKey string
    
    func main() {
    
        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"))
    }
    

    2.3 context的注意事项:

    - 推荐以参数的方式显示传递Context
    - 以Context作为参数的函数方法,应该把Context作为第一个参数,显示传递。
    ​- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
    - Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
    - Context是线程安全的,可以放心的在多个goroutine中传递
    

    相关文章

      网友评论

        本文标题:Golang问题点(三) - Context的问题点

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