golang面试:golang中的context(四)


title: golang中的context(四)
auther: Russshare
toc: true
date: 2021-07-13 19:19:30
tags: [面试, golang, context]
categories: golang面试

前言:

最近在学协程,看见资料说在复杂的网络高并发环境,channel是难以满足我们的使用要求,需要用到context,以前我也看见这个万一,看来真的很重要,于是就学习两篇很厉害的blog学习一下。

https://studygolang.com/articles/9517
https://leileiluoluo.com/posts/golang-context.html

正文

简介

golang 中的创建一个新的 goroutine , 并不会返回像c语言类似的pid,
所有我们不能从外部杀死某个goroutine,所有我就得让它自己结束,
之前我们用 channel + select 的方式,来解决这个问题,但是有些场景实现起来比较麻烦,
例如由一个请求衍生出的各个 goroutine 之间需要满足一定的约束关系,
以实现一些诸如有效期,中止routine树,传递请求全局变量之类的功能。
于是google 就为我们提供一个解决方案,开源了 context 包。
使用 context 实现上下文功能约定需要在你的方法的传入参数的第一个
传入一个 context.Context 类型的变量。

1、场景

我们知道,在Go服务端,每个进入的请求会被其所属goroutine处理。

例如,如下代码,每次请求,Handler会创建一个goroutine来为其提供服务,而且连续请求3次,r的地址也是不同的。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(&r)
        w.Write([]byte("hello"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}
$ go run test.go


$ curl http://localhost:8080/echo
$ curl http://localhost:8080/echo
$ curl http://localhost:8080/echo
0xc000072040
0xc000072048
0xc000072050

而每个请求对应的Handler,常会启动额外的的goroutine进行数据查询或PRC调用等。

而当请求返回时,这些额外创建的goroutine需要及时回收。
而且,一个请求对应一组请求域内的数据可能会被该请求调用链条内的各goroutine所需要。

例如,在如下代码中,当请求进来时,
Handler会创建一个监控goroutine,其会每隔1s打印一句“req is processing”。

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
        // monitor
        go func() {
            for range time.Tick(time.Second) {
                fmt.Println("req is processing")
            }
        }()

        // assume req processing takes 3s
        time.Sleep(3 * time.Second)
        w.Write([]byte("hello"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

假定请求需耗时3s,即请求在3s后返回,我们期望监控goroutine在打印3次“req is processing”后即停止。

但运行发现,监控goroutine打印3次后,其仍不会结束,而会一直打印下去。

问题出在创建监控goroutine后未对其生命周期作控制下面我们使用context作一下控制,即监控程序打印前需检测r.Context()是否已经结束,若结束则退出循环,即结束生命周期。

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
        // monitor
        go func() {
            for range time.Tick(time.Second) {
                select {
                case <-r.Context().Done():
                    fmt.Println("req is outgoing")
                    return
                default:
                    fmt.Println("req is processing")
                }
            }
        }()

        // assume req processing takes 3s
        time.Sleep(3 * time.Second)
        w.Write([]byte("hello"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}

基于如上需求,context包应用而生。

context包可以提供一个请求从API请求边界到各goroutine的请求域数据传递、取消信号及截至时间等能力。

2 Context类型

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Done方法返回一个channel,当Context取消或到达截至时间时,该channel即会关闭。Err方法返回Context取消的原因。

Context自己没有Cancel方法,而且Done channel仅用来接收信号:接收取消信号的函数不应同时是发送取消信号的函数。父goroutine启动子goroutine来做一些子操作,而子goroutine不应用来取消父goroutine。

Context是安全的,可被多个goroutine同时使用。一个Context可以传给多个goroutine,而且可以给所有这些goroutine发取消信号。

若有截至时间,Deadline方法可以返回该Context的取消时间。

Value允许Context携带请求域内的数据,该数据访问必须保障多个goroutine同时访问的安全性。

衍生Context

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

context包提供从已有Context衍生新的Context的能力。这样即可形成一个Context树,
当父Context取消时,所有从其衍生出来的子Context亦会被取消。

Background是所有Context树的根,其永远不会被取消。

使用WithCancel及WithTimeout可以创建衍生的Context,WithCancel可用来取消一组从其衍生的goroutine,WithTimeout可用来设置截至时间。

WithValue提供给Context赋予请求域数据的能力。

下面来看几个对如上方法使用的例子。

1)首先,看一下WitchCancel的使用。

在如下代码中,main函数使用WithCancel创建一个基于Background的ctx

然后启动一个monitor goroutine,该monitor每隔1s打印一句“monitor woring”,

main函数在3s后执行cancel,那么monitor检测到取消信号后即会退出。

package main

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

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // monitor
    go func() {
        for range time.Tick(time.Second) {
            select {
            case <-ctx.Done():
                return
            default:
                fmt.Println("monitor woring")
            }
        }
    }()

    time.Sleep(3 * time.Second)
}
2)再看一个使用WithTimeout的例子,

如下代码中使用WithTimeout创建一个基于Background的ctx,其会在3s后取消。

注意,虽然到截至时间会自动cancel,但cancel代码仍建议加上。

到截至时间而被取消还是被cancel代码所取消,取决于哪个信号发送的早。

package main

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

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    select {
    case <-time.After(4 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}

WithDeadline的使用与WithTimeout相似。

没想好Context的具体使用,可以使用TODO来占位,也便于工具作正确性检查。

3)最后看一下WithValue的使用。

如下代码基于Background创建一个带值的ctx,然后可以根据key来取值。

注意:避免多个包同时使用context而带来冲突,key不建议使用string或其他内置类型,而建议自定义key类型。

package main

import (
    "context"
    "fmt"
)

type ctxKey string

func main() {
    ctx := context.WithValue(context.Background(), ctxKey("a"), "a")

    get := func(ctx context.Context, k ctxKey) {
        if v, ok := ctx.Value(k).(string); ok {
            fmt.Println(v)
        }
    }
    get(ctx, ctxKey("a"))
    get(ctx, ctxKey("b"))
}

最后列一下Context使用规则:

a)勿将Context作为struct的字段使用,而是对每个使用其的函数分别作参数使用,其需定义为函数或方法的第一个参数,一般叫作ctx;

b)勿对Context参数传nil,未想好的使用那个Context,请传context.TODO;

c)使用context传值仅可用作请求域的数据,其它类型数据请不要滥用;

d)同一个Context可以传给使用其的多个goroutine,且Context可被多个goroutine同时安全访问。

最后再重复一边剖析源码

context.Context 接口

context 包的核心

//  context 包里的方法是线程安全的,可以被多个 goroutine 使用    
type Context interface {               
    // 当Context 被 canceled 或是 times out 的时候,Done 返回一个被 closed 的channel      
    Done() <-chan struct{}        

    // 在 Done 的 channel被closed 后, Err 代表被关闭的原因   
    Err() error 

    // 如果存在,Deadline 返回Context将要关闭的时间  
    Deadline() (deadline time.Time, ok bool)

    // 如果存在,Value 返回与 key 相关了的值,不存在返回 nil  
    Value(key interface{}) interface{}
}

我们不需要手动实现这个接口,context 包已经给我们提供了两个,
一个是 Background(),一个是 TODO(),这两个函数都会返回一个 Context 的实例。
只是返回的这两个实例都是空 Context。

主要结构

cancelCtx 结构体继承了 Context ,实现了 canceler 方法:

//*cancelCtx 和 *timerCtx 都实现了canceler接口,实现该接口的类型都可以被直接canceled
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}        

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             // 当其被cancel时将会把err设置为非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
    return c.done
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

//核心是关闭c.done
//同时会设置c.err = err, c.children = nil
//依次遍历c.children,每个child分别cancel
//如果设置了removeFromParent,则将c从其parent的children中删除
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    close(c.done)
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c) // 从此处可以看到 cancelCtx的Context项是一个类似于parent的概念
    }
}

timerCtx 结构继承 cancelCtx

type timerCtx struct {
    cancelCtx //此处的封装为了继承来自于cancelCtx的方法,cancelCtx.Context才是父亲节点的指针
    timer *time.Timer // Under cancelCtx.mu. 是一个计时器
    deadline time.Time
}

valueCtx 结构继承 cancelCtx

type valueCtx struct {
    Context
    key, val interface{}
}
主要方法
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

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

WithCancel 对应的是 cancelCtx ,其中,返回一个 cancelCtx ,
同时返回一个 CancelFunc,
CancelFunc 是 context 包中定义的一个函数类型:type CancelFunc func()。
调用这个 CancelFunc 时,关闭对应的c.done,也就是让他的后代goroutine退出。

WithDeadline 和 WithTimeout 对应的是 timerCtx ,WithDeadline 和 WithTimeout 是相似的,
WithDeadline 是设置具体的 deadline 时间,到达 deadline 的时候,后代 goroutine 退出,
而 WithTimeout 简单粗暴,直接 return WithDeadline(parent, time.Now().Add(timeout))。

WithValue 对应 valueCtx ,WithValue 是在 Context 中设置一个 map,
拿到这个 Context 以及它的后代的 goroutine 都可以拿到 map 里的值。

详细 context 包源码解读:go源码解读

使用原则

使用 Context 的程序包需要遵循如下的原则来满足接口的一致性以及便于静态分析

不要把 Context 存在一个结构体当中,显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为ctx

即使方法允许,也不要传入一个 nil 的 Context ,如果你不确定你要用什么 Context 的时候传一个 context.TODO

使用 context 的 Value 相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数

同样的 Context 可以用来传递到不同的 goroutine 中,Context 在多个goroutine 中是安全的

使用示例

例子copy自: 关于 Golang 中的 context 包的介绍

package main

import (
    "fmt"
    "time"
    "golang.org/x/net/context"
)

// 模拟一个最小执行时间的阻塞函数
func inc(a int) int {
    res := a + 1                // 虽然我只做了一次简单的 +1 的运算,
    time.Sleep(1 * time.Second) // 但是由于我的机器指令集中没有这条指令,
    // 所以在我执行了 1000000000 条机器指令, 续了 1s 之后, 我才终于得到结果。B)
    return res
}

// 向外部提供的阻塞接口
// 计算 a + b, 注意 a, b 均不能为负
// 如果计算被中断, 则返回 -1
func Add(ctx context.Context, a, b int) int {
    res := 0
    for i := 0; i < a; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }
    for i := 0; i < b; i++ {
        res = inc(res)
        select {
        case <-ctx.Done():
            return -1
        default:
        }
    }
    return res
}

func main() {
    {
        // 使用开放的 API 计算 a+b
        a := 1
        b := 2
        timeout := 2 * time.Second
        ctx, _ := context.WithTimeout(context.Background(), timeout)
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
    {
        // 手动取消
        a := 1
        b := 2
        ctx, cancel := context.WithCancel(context.Background())
        go func() {
            time.Sleep(2 * time.Second)
            cancel() // 在调用处主动取消
        }()
        res := Add(ctx, 1, 2)
        fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_45264425/article/details/132200034