Go并发编程-Context包

一、前言

在Go服务器中,对于每个新来的请求都会开启新的goroutine来进行处理,每个请求处理器通常又会开另外的新goroutine来访问后端服务(比如数据库和rpc服务)。Go并发编程-Context包_第1张图片

处理每个请求的这些goroutine集合通常会需要存取请求级别的变量(类似于Java中threadlocal类型的变量),比如标识请求用户身份的userId,鉴权token,请求的超时时间等。当一个请求被取消或者超时,所有为这个请求服务的goroutine集合应该迅速的终止以便系统能及时回收他们使用的这些资源。

在go语言中,提供了一个context包,context包定义了Context类型,这个类型内部携带了处理一个请求过程中所有goroutines涉及的请求作用域的变量、取消信号、请求截止日期信息。

Context中定义了下面方法,这些方法是可以被多个goroutine同时调用的(线程安全的)

type Context interface {
        //返回两个参数,第一个是返回什么时候工作被完成(这代表了上下文被取消了);第二参数当没有设置截止时间时候返回false
        Deadline() (deadline time.Time, ok bool)
        //1. 返回一个通道,这个通道当上下文被取消或者到了截止时间会被关闭。
        //2. 当使用WithCancel返回的上下文时候 ,Done通道当cancel方法被调用后会被关闭
        //3. 当使用WithDeadline 返回的上下文时候,Done 通道当达到截止时间后会被关闭;
        //4. 当使用WithTimeout返回的上下文时候,Done通道当timeout超时后会被关闭.
        //5. Done一般被用在select语句:
        Done() <-chan struct{}
        //1.如果Done通道没有被关闭,则调用err方法返回nil
        //2.如果Done通道被关闭,则Err返回一个非nil的错误,这个错误解释关闭的原因:或者是由于上下文被关闭了,或者是因为截止时间到期了
        //3.
        Err() error
        // 返回与上下文关联的key的值,或者返回nil如果该key没有关联到该上下文
        // 一般用上下文value仅仅是为了在多个goroutine间传递请求作用域的变量。
        Value(key interface{}) interface{}
}

go服务器每当接受新的请求时候就会创建一个Context对象,当服务器接受请求后交给请求处理器进行处理时候要把创建的Context传递下去。整个请求处理的函数调用链中必须要把Context对象传递下去,另外传递过程中Context对象可以使用其子类比如WithCancel, WithDeadline, WithTimeout, WithValue创建的Context对象替换;当一个Context被取消了,那么所有从其派生的子类的Context也会被取消。

函数WithCancel, WithDeadline, WithTimeout入参是一个Context对象,返回值有两个:一个派生于入参的子Context和一个CancelFunc函数。调用CancelFunc函数会取消子Context和其子Context对象(如果其有子对象的话),并且会从其父Context中移除对该子Context的引用,停止与其关联的timer对象。如果忘记调用CancelFunc方法会导致子Context和子Context的子Context泄漏,但是这个泄漏会被避免如果其父Context被取消了或者timer超时激活了。

为了保持接口一致性和能够使用静态分析工具检查Context传播链路,在使用Context时候需要遵循下面的规则:不要存储Context对象在一个结构体类型内部,而是要显示的传递Context对象在每个需要他的函数的入参内,另外一般Context对象作为入参的第一个参数,并且命名为ctx:

func DoSomething(ctx context.Context, arg Arg1,arg Arg2) error {
    // ... use ctx ...
}

二 、Context包-变量

  • Canceled变量

var Canceled = errors.New("context canceled")

当一个context对象被取消后Context.Err会返回这个错误

  • DeadlineExceeded变量

var DeadlineExceeded error = deadlineExceededError{}

当一个context的deadline到后(超时了)会从Context.Err返回这个错误

三、Context包-方法

  • WithCancel 方法

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

WithCancel返回parent的一个复制对象,并且这个复制对象里面含有一个新的Done通道,另外还返回一个cancel的函数;如果返回的cancel函数被调用或者parent的context的Done通道被关闭,则该函数返回的context的Done通道会被关闭;

关闭返回的context会释放与其关联的资源,因此写代码时候应该在使用完毕context对象后,尽早取消context对象。

下面看个示例以便加深理解:

package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    // 1.gen函数在新的goroutine内产生整数,并发送这些整数到返回的无缓冲通道内
    gen := func(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done(): //1.1阻塞直到ctx的Done通道被取消
                    fmt.Println(ctx.Err()) //1.2ctx被取消后,打印错误信息
                    return                 //
                case dst <- n: //1.3向返回的通道写入数据
                    n++ //1.4递增数据
                }
            }
        }()
        return dst
    }
    //2.创建一个可以被取消的context
    ctx, cancel := context.WithCancel(context.Background())
    //3.从gen函数返回的通道内读取5个元素并打印
    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            cancel() //3.1如果当了第5个元素则取消ctx,则ctx的Done通道会被取消并返回0值
            break
        }
    }
    //4.main函数所在goroutine休眠1s
    time.Sleep(1 * time.Second)
}

如上代码 gen函数在新的goroutine内产生整数,并发送这些整数到返回的无缓冲通道内

main函数代码2创建一个可以被取消的ctx,然后从gen函数返回的通道内读取元素,当读取完毕5个元素后,调用了3.1取消了ctx,取消ctx后,ctx管理的Done通道就会被关闭,所以gen函数中的代码1.1就会执行,然后打印输出错误信息,然后gen内开启的gorotine就正常退出了,以避免gen函数内的goroutine泄漏。运行上面代码会输出:

1
2
3
4
5
context canceled
  • WithDeadline方法

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

WithDeadline返回父上下文parent的副本,其截止日期调整为不晚于参数d。如果父级的截止日期早于d,则WithDeadline(parent,d)在语义上等同于parent。返回的上下文的Done通道会被关闭当下面情况发生时候:截止时间到期了,调用返回的取消函数CancelFunc时或父上下文p的Done通道关闭时。

关闭返回的context会释放与其关联的资源,因此写代码时候应该在使用完毕context对象后,尽早取消context对象。

下面看个示例以便加深理解:

import (
    "context"
    "fmt"
    "time"
)
func main() {
    //1.截止时间为当前时间加上100ms
    d := time.Now().Add(100 * time.Millisecond)
    //2.基于空context创建一个deadline上下文
    ctx, cancel := context.WithDeadline(context.Background(), d)
    //3.等main函数结束后,上下文会被关闭,如果不及时关闭会导致上下文ctx和其父context存在的周期比我们想要的长
    defer cancel()
    //4.select块
    select {
    case <-time.After(1 * time.Second)://4.1如果1s上下文还没被取消,则超时打印
        fmt.Println("overslept")
    case <-ctx.Done()://4.2如果上下文ctx在1s内被取消或者超时了,则打印错误
        fmt.Println(ctx.Err())
    }
}

上面代码1创建了一个截止时间,也就是从当前时间等待100ms就过期。代码2则基于空context和截止时间创建了一个子上下文ctx。代码3则等main函数结束后取消上下文ctx 代码4select块作用是阻塞main函数所在goroutine,解除阻塞条件是case1等待1s超时后,或者case2当ctx被主动取消或者被动超时(超时时间为100ms). 运行上面代码,由于ctx截止时间为当前时间加上100ms,所以ctx会在100ms后超时,然后Done通道会被关闭,所以case2会返回,然后通过ctx.Err获取错误信息:"context deadline exceeded"

  • WithTimeout 方法

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

WithTimeout等价于 WithDeadline(parent,time.Now().Add(timeout))

关闭返回的context会释放与其关联的资源,因此写代码时候应该在使用完毕context对象后,尽早取消context对象。

WithDeadline中的例子等价于下面例子:

package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    //1.基于空context创建一个deadline上下文
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    //2.等main函数结束后,上下文会被关闭,如果不及时关闭会导致上下文ctx和其父context存在的周期比我们想要的长
    defer cancel()
    //3.select块
    select {
    case <-time.After(1 * time.Second)://3.1如果1s上下文还没被取消,则超时打印
        fmt.Println("overslept")
    case <-ctx.Done()://3.2如果上下文ctx在1s内被取消或者超时了,则打印错误
        fmt.Println(ctx.Err())
    }
}
  • Background 方法

func Background() Context

Background返回一个非nil的空Context。它永远不会被取消,没有关联的value,也没有截止日期。它通常由主函数,初始化和测试使用,并作为传入请求的顶级Context。

  • WithValue 方法

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

WithValue方法返回一个父上下文parent的拷贝,这个拷贝内关联了key对应的val

使用上下文值仅仅被用于在多goroutine间传递请求作用域的值,另外上下文中的key必须是可比较的,另外为了避免context包之间的冲突,key不应该是字符串类型或任何其他内置类型. WithValue的用户应该为key键定义他们自己的类型。

package main
import (
    "context"
    "fmt"
    "sync"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    //1.自定义类型
    type String string
    //2.创建函数,内部从ctx内获取变量k的值并打印
    f := func(ctx context.Context, k String) {
        defer wg.Done()
        if v := ctx.Value(k); v != nil {
            fmt.Println("found value:", v)
            return
        }
        fmt.Println("key not found:", k)
    }
    //3.创建k,并关联到上下文
    k := String("language")
    ctx := context.WithValue(context.Background(), k, "Go")
    //4.开启gorutine
    go f(ctx, k)
    go f(ctx, String("color"))
    wg.Wait()
}

如上代码1自定义了一个类型String

代码2创建了一个函数f,其作用是从参数1上下文对象ctx中查找key为k的变量的值,如果存在则打印

代码3使用自定义类型创建了一个变量,并关联k到上下文上,并返回一个新的上下文对象

代码4开启两个goroutine调用函数f

另外这里使用sync.WaitGroup做同步,main所在gorotine等两个开启的gorotine结束后才结束。

运行上面代码会输出:

found value: Go
key not found: color

假期在家无聊?那就免费学习下Go语言吧!!!

Go并发编程-并发与并行

Go并发编程-并发编程难在哪里

Go并发编程-线程模型

Go并发编程-内存模型

Go并发编程-goroutine轻量级的线程

Go并发编程-runtime包

Go并发编程-互斥锁

Go并发编程-读写锁

Go并发编程-条件变量

Go并发编程-WaitGroup

Go并发编程-Channel

Go并发编程-通道与timer包

你可能感兴趣的:(Go并发编程-Context包)