深入理解Golang 中的Context包

context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念。context.Context深度支持Golang的高并发。

1.Goroutine 和channel。

在理解context包之前,应该首先熟悉Goroutine和Channel,能加深对context的理解。

1.1 Goroutine

Goroutine是一个轻量级的执行线程,多个Goroutine比一个线程轻量,所以管理Goroutine消耗的资源相对更少。Goroutine是Go中最基本的执行单元,每一个Go程序至少有一个Goroutine:主Goroutine。程序启动时会自动创建。为了能更好的理解Goroutine,先来看一看线程Thread与协程Coroutine的概念。

线程(Thread)

    线程是一种轻量级进程,是CPU调度的最小单位。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属于一个进程的其他线程共享进程所拥有的全部资源。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈。线 程 的 切 换 一 般 由 操 作 系 统 调 度 \color{red}{线程的切换一般由操作系统调度}线程的切换一般由操作系统调度。

协程(Coroutine)

   协程又称为微线程,与子例程一样,协程也是一种程序组建,相对子例程而言,协程更为灵活,但在实践中使用没有子例程那样广泛。和线程类似,共享堆,不共享栈,协 程 的 切 换 一 般 由 程 序 员 在 代 码 中 显 式 控 制 \color{red}{协程的切换一般由程序员在代码中显式控制}协程的切换一般由程序员在代码中显式控制。他避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时,Gorotine可以运行在一个或多个线程上。

使用示例

package main

import (

        "fmt"

)

func Hello() {

        fmt.Println("Hello everyBody, I'm WDS")

}

func main() {

        go Hello()

        fmt.Println("Example")

}

输出结果:

Example

Hello everyBody, I'm WDS

1.3Channel

  Channel就是多个Goroutine 之间的沟通渠道。当我们想要将结果或错误,或任何其他类型的信息从一个 goroutine 传递到另一个 goroutine 时就可以使用通道。通道是有类型的,可以是 int 类型的通道接收整数或错误类型的接收错误等。

  假设有个 int 类型的通道 ch,如果想发一些信息到这个通道,语法是 ch <- 1,如果想从这个通道接收一些信息,语法就是 var := <-ch。这将从这个通道接收并存储值到 var 变量。

通过改善1.2中的代码片段,证明通道的使用确保了 goroutine 执行完成并将值返回给 main 。

package main

import (

        "fmt"

)

func Hello(ch chan int) {

        fmt.Println("Hello everyBody, I'm WDS")

        ch <- 1

}

func main() {

        ch := make(chan int)

        go Hello(ch)

        <-ch

        fmt.Println("Example")

}

输出结果:

Hello everyBody, I'm WDS

Example

 

2.Context应用场景

package main

import (

        "fmt"

        "log"

        "net/http"

)

func main() {

        http.HandleFunc("/", SayHello) //设置访问的路由

        log.Fatalln(http.ListenAndServe(":8080", nil))

}

func SayHello(writer http.ResponseWriter, request *http.Request) {

        fmt.Println(&request)

        writer.Write([]byte("Hi,New Request Comes"))

}

上述代码每次请求,handler会创建一个Goroutine 为其提供服务、

    在真实应用场景中,每个请求对应的Handler,常会启动额外的的goroutine进行数据查询或PRC调用等。这里可以理解为每次请求的业务处理逻辑中,需要多次访问其他服务,而这些服务是可以并发执行的,即主Gorontine内的多个Goroutine并存。而且,当请求返回时,这些额外创建的goroutine需要及时回收。此外,一个请求对应一组请求域内的数据可能会被该请求调用链条内的各goroutine所需要现在对上面的代码添加一点东西,当请求进来时,hanler 创建一个监控goroutinue,这样每隔1s打印一个Hello World。

package main

import (

        "fmt"

        "log"

        "net/http"

        "time"

)

func main() {

        http.HandleFunc("/", SayHello) //设置访问的路由

        log.Fatalln(http.ListenAndServe(":8080", nil))

}

func SayHello(writer http.ResponseWriter, request *http.Request) {

        fmt.Println(&request)

        go func() {

                for range time.Tick(time.Second) {

                        fmt.Println("Current request is in progress")

                }

        }()

        time.Sleep(2 * time.Second)

        writer.Write([]byte("Hi,New Request Comes"))

}

在这里假定需要耗时2s,但在请求2s后返回,我们期望goroutine 在两次打印Hello World后停止。但运行发现,监控goroutine 打印后,其仍然不会结束,会一直打印下去。问题出在创建goroutine后,未对其生命周期作控制,下面我们使用context作一下控制,即监控程序打印前需检测request.Context()是否已经结束,若结束则退出循环,即结束生命周期。

示例代码:

package main

import (

        "fmt"

        "log"

        "net/http"

        "time"

)

func main() {

        http.HandleFunc("/", SayHello) //设置访问的路由

        log.Fatalln(http.ListenAndServe(":8080", nil))

}

func SayHello(writer http.ResponseWriter, request *http.Request) {

        fmt.Println(&request)

        go func() {

                for range time.Tick(time.Second) {

                        select {

                        case <-request.Context().Done():

                                fmt.Println("request is outgoing")

                                return

                        default:

                                fmt.Println("Hello World")

                        }

                }

        }()

        time.Sleep(2 * time.Second)

        writer.Write([]byte("Hi,New Request Comes"))

}

输出结果:

0xc00008e008

Hello World

request is outgoing

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

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

3.Context详解

   在 Go 语言中 context 包允许传递一个 “context” 到程序中。 Context 如超时或截止日期(deadline)或通道,来指示停止运行和返回。例如,如果正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统通常是个好主意。因为,如果依赖的API运行缓慢,不希望在系统上备份(back up)请求,因为它可能最终会增加负载并降低所有请求的执行效率。导致级联效应。这是超时或截止日期 context 派上用场的地方。

3.1设计原理

   Go 语言中的每一个请求的都是通过一个单独的 Goroutine 进行处理的,HTTP/RPC 请求的处理器往往都会启动新的 Goroutine 访问数据库和 RPC 服务,我们可能会创建多个 Goroutine 来处理一次请求,而 Context 的主要作用就是在不同的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期。

每一个 Context 都会从最顶层的 Goroutine 一层一层传递到最下层,这也是 Golang 中上下文最常见的使用方式,如果没有 Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去:

当最上层的 Goroutine 因为某些原因执行失败时,下两层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 Context 时,就可以在下层及时停掉无用的工作减少额外资源的消耗:在这里,来看另一个例子:

package main

import (

        "context"

        "fmt"

        //        "log"

        //        "net/http"

        "time"

)

func main() {

        ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

        defer cancel()

        go HelloHandle(ctx, 500*time.Millisecond)

        select {

        case <-ctx.Done():

                fmt.Println("Hello Handle", ctx.Err())

        }

}

func HelloHandle(ctx context.Context, duration time.Duration) {

        select {

        case <-ctx.Done():

                fmt.Println(ctx.Err())

        case <-time.After(duration):

                fmt.Println("Hello World with", duration)

        }

}

上面的代码,因为过期时间大运处理时间,所以我们有足够的时间处理改请求,输出结果如下:

Hello World with 500ms

Hello Handle context deadline exceeded

HelloHandle函数并没有进入超时的select分支,但是main函数的select却会等待context.Context的超时并打印出Hello Handle context deadline exceeded。如果我们将处理请求的时间增加至2000ms,程序就会因为上下文过期而被终止。

Hello Handle context deadline exceeded

context deadline exceeded

3.2 Context接口

context.Context 是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个需要实现的方法,其中包括:

Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;

Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后关闭,多次调用 Done 方法会返回同一个 Channel;

Err — 返回 context.Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;如果 context.Context 被取消,会返回 Canceled 错误;如果 context.Context 超时,会返回 DeadlineExceeded 错误;

Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

type Context interface {

    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}

}

4.总结

1context.Background 只应用在最高等级,作为所有派生 context 的根。

2.context取消是建议性的,这些函数可能需要一些时间来清理和退出。

3.不要把Context放在结构体中,要以参数的方式传递。

4.以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。

5.给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO

6.Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。

7.context.Value应该很少使用,它不应该被用来传递可选参数。这使得 API 隐式的并且可以引起错误。取而代之的是,这些值应该作为参数传递。

8.Context是线程安全的,可以放心的在多个goroutine中传递。同一个Context可以传给使用其的多个goroutine,且Context可被多个goroutine同时安全访问。

9.Context 结构没有取消方法,因为只有派生 context 的函数才应该取消 context。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值