Go:context

时间:Feb. 17, 2020 分类:

目录:

一个goroutine的场景

当启动了一个goroutine的时候,goroutine又需要启动一个goroutine,如何合理的退出并释放这个goroutine?

全局变量的方式

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup
var exit bool

func worker() {
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        if exit {
            break
        }
    }
    wg.Done()
}

func main() {
    wg.Add(1)
    go worker()
    time.Sleep(time.Second * 3)
    exit = true   
    wg.Wait()
    fmt.Println("over")
}

全局变量的问题是

  1. 使用全局变量在跨包调用时不容易统一
  2. 如果worker中再启动goroutine,就不太好控制了

通道方式

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func worker(exitChan chan struct{}) {
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-exitChan:
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    var exitChan = make(chan struct{})
    wg.Add(1)
    go worker(exitChan)
    time.Sleep(time.Second * 3)
    exitChan <- struct{}{}
    close(exitChan)
    wg.Wait()
    fmt.Println("over")
}

通道的问题是使用全局变量在跨包调用时不容易实现规范和统一,需要维护一个共用的channel

官方版解决方式

package main

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

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 3)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}

对于goroutine再调用goroutine的情况

package main

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

var wg sync.WaitGroup

func worker(ctx context.Context) {
    go worker2(ctx)
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            break LOOP
        default:
        }
    }
    wg.Done()
}

func worker2(ctx context.Context) {
LOOP:
    for {
        fmt.Println("worker2")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 3)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}

Context

介绍

go在1.7版本引入了context,简化用于处理单个请求中多个goruntine之间的请求域之间的数据,取消信号,截止时间等

context.Context是一个接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline返回当前context的取消时间
  • Done返回一个channel,这个channel会在当前工作完后或者上下文取消的时候关闭,多次调用会返回一个相同的Channel
  • Err返回当前context结束的原因
  • Value方法会从Context中返回键对应的值,对于同一个上下文,多次调用value并传入相同的key会返回相同的结果

对于Err

  • 在Done返回的channel被关闭返回nil
  • Context取消会返回Canceled错误
  • Context超时就会返回DeadlineExceeded错误

Background()和TODO()

Go内置函数Background()和TODO()分别返回了一个实现Context接口的background和todo

Background()主要用在main函数,初始化和测试代码中,作为Context树的顶层Context,或者说是根Context

TODO()暂时还没有用途

With系列函数

WithCancel

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

使用方式参考上边官方示例

WithDeadline

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

这里需要额外传入deadline,当deadline到期时会调用Done()

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
    // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
    defer cancel()

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

WithTimeout

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

这里是达到timeout,会调用Channel,常用于数据库和网络超时

ackage main

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

// context.WithTimeout

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("db connecting ...")
        time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自动调用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 设置一个50毫秒的超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}

WithValue

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

将数据与Context建立关系

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithValue

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
    key := TraceCode("TRACE_CODE")
    traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
    if !ok {
        fmt.Println("invalid trace code")
    }
LOOP:
    for {
        fmt.Printf("worker, trace code:%s\n", traceCode)
        time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自动调用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 设置一个50毫秒的超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
    ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}

Context注意事项

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