这里填写标题

  • 1. Golang context.Context
    • 1.1. 内容前导
    • 1.2. 基础知识
      • 1.2.1. Context 接口
      • 1.2.2. 顶层 Context
      • 1.2.3. 子 Context
      • 1.2.4. 元数传递
    • 1.3. 知识扩展
    • 1.4. 实战场景: 上下游调用
    • 1.5. 总结

1. Golang context.Context

1.1. 内容前导

回顾之前的知识, 我们先看一个关于 WaitGroup 的示例:

func main() {    var wg sync.WaitGroup    wg.Add(2)    go func() {        time.Sleep(2*time.Second)        fmt.Println("1 号完成")        wg.Done()    }()    go func() {        time.Sleep(2*time.Second)        fmt.Println("2 号完成")        wg.Done()    }()    wg.Wait()    fmt.Println("好了, 大家都干完了, 放工")}

示例比较简单, main 协程等待两个 goroutine 的结束。如果是希望主协程关闭, 通知 goutoutine 关闭, 我们可以使用 select + chan 的方式:

func main() {    stop := make(chan bool)    go func() {        for {            select {            case <-stop:                fmt.Println("监控退出, 停止了。..")                return            default:                fmt.Println("goroutine 监控中。..")                time.Sleep(2 * time.Second)            }        }    }()    time.Sleep(10 * time.Second)    fmt.Println("可以了, 通知监控停止")    stop<- true    //为了检测监控过是否停止, 如果没有监控输出, 就表示停止了    time.Sleep(5 * time.Second)}

这种 chan+select 的方式, 是比较优雅的结束一个 goroutine 的方式, 不过这种方式也有局限性, 如果有很多 goroutine 都需要控制结束怎么办呢? 如果这些 goroutine 又衍生了其他更多的 goroutine 怎么办呢? 如果一层层的无穷尽的 goroutine 呢? 这就非常复杂了, 即使我们定义很多 chan 也很难解决这个问题, 因为 goroutine 的关系链就导致了这种场景非常复杂。上面说的这种场景是存在的, 比如一个网络请求 Request, 每个 Request 都需要开启一个 goroutine 做一些事情, 这些 goroutine 又可能会开启其他的 goroutine。所以我们需要一种可以跟踪 goroutine 的方案, 才可以达到控制他们的目的, 这就是 Go 语言为我们提供的 Context, 称之为上下文非常贴切, 它就是 goroutine 的上下文, 我们对上面示例进行改造:

func main() {    ctx, cancel := context.WithCancel(context.Background())    go func(ctx context.Context) {        for {            select {            case <-ctx.Done():                fmt.Println("监控退出, 停止了。..")                return            default:                fmt.Println("goroutine 监控中。..")                time.Sleep(2 * time.Second)            }        }    }(ctx)    time.Sleep(10 * time.Second)    fmt.Println("可以了, 通知监控停止")    cancel()    //为了检测监控过是否停止, 如果没有监控输出, 就表示停止了    time.Sleep(5 * time.Second)}
当执行 cancel() 时, goroutine 会接收到 ctx.Done() 的信号, 协程退出, 对于控制多个 goroutine 的示例如下:
func main() {    ctx, cancel := context.WithCancel(context.Background())    go watch(ctx,"【监控 1】")    go watch(ctx,"【监控 2】")    go watch(ctx,"【监控 3】")    time.Sleep(10 * time.Second)    fmt.Println("可以了, 通知监控停止")    cancel()    //为了检测监控过是否停止, 如果没有监控输出, 就表示停止了    time.Sleep(5 * time.Second)}func watch(ctx context.Context, name string) {    for {        select {        case <-ctx.Done():            fmt.Println(name,"监控退出, 停止了。..")            return        default:            fmt.Println(name,"goroutine 监控中。..")            time.Sleep(2 * time.Second)        }    }}

1.2. 基础知识

1.2.1. Context 接口

Context 的接口定义的比较简洁, 我们看下这个接口的方法:

type Context interface {    Deadline() (deadline time.Time, ok bool)    Done() <-chan struct{}    Err() error    Value(key interface{}) interface{}}

这个接口共有 4 个方法, 了解这些方法的意思非常重要, 这样我们才可以更好的使用他们:

  • Deadline 方法是获取设置的截止时间的意思, 第一个返回式是截止时间, 到了这个时间点, Context 会自动发起取消请求; 第二个返回值 ok==false 时表示没有设置截止时间, 如果需要取消的话, 需要调用取消函数进行取消。
  • Done 方法返回一个只读的 chan, 类型为 struct{}, 我们在 goroutine 中, 如果该方法返回的 chan 可以读取, 则意味着 parent context 已经发起了取消请求, 我们通过 Done 方法收到这个信号后, 就应该做清理操作, 然后退出 goroutine, 释放资源。
  • Err 方法返回取消的错误原因, 因为什么 Context 被取消。
  • Value 方法获取该 Context 上绑定的值, 是一个键值对, 所以要通过一个 Key 才可以获取对应的值, 这个值一般是线程安全的。

1.2.2. 顶层 Context

Context 接口并不需要我们实现, Go 内置已经帮我们实现了 2 个, 我们代码中最开始都是以这两个内置的作为最顶层的 partent context, 衍生出更多的子 Context:

var (    background = new(emptyCtx)    todo       = new(emptyCtx))func Background() Context {    return background}func TODO() Context {    return todo}

一个是 Background, 主要用于 main 函数、初始化以及测试代码中, 作为 Context 这个树结构的最顶层的 Context, 也就是根 Context。一个是 TODO, 它目前还不知道具体的使用场景, 如果我们不知道该使用什么 Context 的时候, 可以使用这个。他们两个本质上都是 emptyCtx 结构体类型, 是一个不可取消, 没有设置截止时间, 没有携带任何值的 Context。

type emptyCtx intfunc (*emptyCtx) Deadline() (deadline time.Time, ok bool) {    return}func (*emptyCtx) Done() <-chan struct{} {    return nil}func (*emptyCtx) Err() error {    return nil}func (*emptyCtx) Value(key interface{}) interface{} {    return nil}

这就是 emptyCtx 实现 Context 接口的方法, 可以看到, 这些方法什么都没做, 返回的都是 nil 或者零值。

1.2.3. 子 Context

有了如上的根 Context, 那么是如何衍生更多的子 Context 的呢? 这就要靠 context 包为我们提供的 With 系列的函数了:

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, val interface{}) Context

这四个 With 函数, 接收的都有一个 partent 参数, 就是父 Context, 我们要基于这个父 Context 创建出子 Context 的意思, 这种方式可以理解为子 Context 对父 Context 的继承, 也可以理解为基于父 Context 的衍生。通过这些函数, 就创建了一颗 Context 树, 树的每个节点都可以有任意多个子节点, 节点层级可以有任意多个。

  • WithCancel 函数, 传递一个父 Context 作为参数, 返回子 Context, 以及一个取消函数用来取消 Context。
  • WithDeadline 函数, 和 WithCancel 差不多, 它会多传递一个截止时间参数, 意味着到了这个时间点, 会自动取消 Context, 当然我们也可以不等到这个时候, 可以提前通过取消函数进行取消。
  • WithTimeout 和 WithDeadline 基本上一样, 这个表示是超时自动取消, 是多少时间后自动取消 Context 的意思。
  • WithValue 函数和取消 Context 无关, 它是为了生成一个绑定了一个键值对数据的 Context, 这个绑定的数据可以通过 Context.Value 方法访问到, 后面我们会专门讲。

大家可能留意到, 前三个函数都返回一个取消函数 CancelFunc, 这就是取消函数的类型, 该函数可以取消一个 Context, 以及这个节点 Context 下所有的所有的 Context, 不管有多少层级。

1.2.4. 元数传递

通过 Context 我们也可以传递一些必须的元数据, 这些数据会附加在 Context 上以供使用。

var key string="name"func main() {    ctx, cancel := context.WithCancel(context.Background())    //附加值    valueCtx:=context.WithValue(ctx,key,"【监控 1】")    go watch(valueCtx)    time.Sleep(10 * time.Second)    fmt.Println("可以了, 通知监控停止")    cancel()    //为了检测监控过是否停止, 如果没有监控输出, 就表示停止了    time.Sleep(5 * time.Second)}func watch(ctx context.Context) {    for {        select {        case <-ctx.Done():            //取出值            fmt.Println(ctx.Value(key),"监控退出, 停止了。..")            return        default:            //取出值            fmt.Println(ctx.Value(key),"goroutine 监控中。..")            time.Sleep(2 * time.Second)        }    }}

在前面的例子, 我们通过传递参数的方式, 把 name 的值传递给监控函数。在这个例子里, 我们实现一样的效果, 但是通过的是 Context 的 Value 的方式。我们可以使用 context.WithValue 方法附加一对 K-V 的键值对, 这里 Key 必须是等价性的, 也就是具有可比性; Value 值要是线程安全的。这样我们就生成了一个新的 Context, 这个新的 Context 带有这个键值对, 在使用的时候, 可以通过 Value 方法读取 ctx.Value(key)。

1.3. 知识扩展

这里我们主要先讨论一下撤销的操作。Done 方法会返回一个元素类型为 struct{}的接收通道, 不过, 这个接收通道的用途并不是传递元素值, 而是让调用方去感知"撤销"当前 Context 值的那个信号, 一旦当前的 Context 值被撤销, 这里的接收通道就会被立即关闭, 因为对于一个未包含任何元素值的通道来说, 它的关闭会使任何针对它的接收操作立即结束。这里解释的可能有点绕, 或者换句话来说, 如果 Context 取消的时候, 它其实主要是关闭 chan, 关闭的 chan 是可以读取的, 所以只要可以读取的时候, 就意味着可以通过 Done 收到 Context 取消的信号了。除了让 Context 值的使用方感知到撤销信号, 让它们得到"撤销"的具体原因, 有时也是很有必要的。后者即是 Context 类型的 Err 方法的作用。该方法的结果是 error 类型的, 并且其值只可能等于 context.Canceled 变量的值, 或者 context.DeadlineExceeded 变量的值, 我们看一个经典用法:

func Stream(ctx context.Context, out chan<- Value) error {    for {          v, err := DoSomething(ctx)          if err != nil {                  return err          }          select {          case <-ctx.Done():                  return ctx.Err()          case out <- v:          }      }  }

我们再讨论撤销信号是如何在上下文树中传播的, 在撤销函数被调用之后, 对应的 Context 值会先关闭它内部的接收通道, 也就是它的 Done 方法会返回的那个通道。然后, 它会向它的所有子值(或者说子节点)传达撤销信号, 这些子值会如法炮制, 把撤销信号继续传播下去。最后, 这个 Context 值会断开它与其父值之间的关联。先看一幅图:
我们通过调用 context 包的 WithDeadline 函数或者 WithTimeout 函数生成的 Context 值也是可撤销的。它们不但可以被手动撤销, 还会依据在生成时被给定的过期时间, 自动地进行定时撤销, 这里定时撤销的功能是借助它们内部的计时器来实现的。当过期时间到达时, 这两种 Context 值的行为与 Context 值被手动撤销时的行为是几乎一致的, 只不过前者会在最后停止并释放掉其内部的计时器。最后要注意, 通过调用 context.WithValue 函数得到的 Context 值是不可撤销的, 撤销信号在被传播时, 若遇到它们则会直接跨过, 并试图将信号直接传给它们的子值。

1.4. 实战场景: 上下游调用

package mainimport (    "context"    "fmt"    "math/rand"    "time")// 作用: 1. 随机 sleep 一会; 2. 如果入参 ch 不为空, 会把 sleep 的时间给到 chfunc sleepRandom(fromFunction string, ch chan int) {    defer func() { fmt.Println(fromFunction, "sleepRandom complete") }()    seed := time.Now().UnixNano()    r := rand.New(rand.NewSource(seed))    randomNumber := r.Intn(100)    sleeptime := randomNumber + 100    fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms")    time.Sleep(time.Duration(sleeptime) * time.Millisecond)    fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms")    if ch != nil {        ch <- sleeptime    }}func sleepRandomContext(ctx context.Context, ch chan bool) {    defer func() {        fmt.Println("sleepRandomContext complete")        // 通过 channel, 通知上游执行完毕        ch <- true    }()    sleeptimeChan := make(chan int)    // 开启新的协程 G2, 让该协程执行逻辑, 执行完毕后, 通过 sleeptimeChan 通知执行完毕    go sleepRandom("sleepRandomContext", sleeptimeChan)    select {    case <-ctx.Done():        // 场景 1: main() 调用 cancelFunction()        // 场景 2: doWorkContext() 调用 cancelFunction()        // 场景 3: doWorkContext() 自动超时        fmt.Println("sleepRandomContext: Time to return")    case sleeptime := <-sleeptimeChan:        // 当新的协程 G2 执行完毕, 调用 ch<-sleeptime 时        fmt.Println("Slept for ", sleeptime, "ms")    }}func doWorkContext(ctx context.Context) {    // 生成新的 ctx, 超时时间为 150ms    ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond)    defer func() {        fmt.Println("doWorkContext complete")        // 下游所有的 ctx 都会关闭        cancelFunction()    }()    ch := make(chan bool)    // 启动新的协程 G1    go sleepRandomContext(ctxWithTimeout, ch)    select {    case <-ctx.Done():        // 当 main 退出, 调用 main 的 cancelFunction() 时        fmt.Println("doWorkContext: Time to return")    case <-ch:        // 当新的协程 G1 退出, 执行 ch<-true 时        fmt.Println("sleepRandomContext returned")    }}func main() {    ctx := context.Background()    ctxWithCancel, cancelFunction := context.WithCancel(ctx)    defer func() {        fmt.Println("Main Defer: canceling context")        // 下游所有的 ctx 都会关闭        cancelFunction()    }()    go func() {        // main 函数 sleep 一会        sleepRandom("Main", nil)        // 下游所有的 ctx 都会关闭        cancelFunction()        fmt.Println("Main Sleep complete. canceling context")    }()   doWorkContext(ctxWithCancel)}

对于上面这个示例, 我描述一下每种场景:

  • 场景 1: main 函数调用 cancelFunction() 后, main() 会直接退出, 同时 doWorkContext 和 sleepRandomContext 函数会同时调用里面的 ctx.Done() 操作, 全部一起退出;
  • 场景 2: doWorkContext 函数超时 150ms 后, sleepRandomContext 函数会直接执行 ctx.Done() 操作, 然后 sleepRandomContext 函数退出前执行 ch <- true, doWorkContext 函数接收到 case <-ch 的信号后, doWorkContext() 退出, main() 退出;
  • 场景 3: sleepRandomContext 函数执行 sleepRandom(), 当 sleepRandom 执行 ch <- sleeptime 后, sleepRandomContext 通过 sleeptime := <-sleeptimeChan 收到信号后, 程序退出, 退出前会执行 ch <- true, 然后 doWorkContext 函数接收到 case <-ch 的信号后, doWorkContext() 退出, main() 退出;
  • 场景 4: main() 异常, 通过 defer 执行 cancelFunction() 后, main() 退出, 后面逻辑同"场景 1";
  • 场景 5: doWorkContext() 异常, 通过 defer 执行 cancelFunction() 后, sleepRandomContext 函数会直接执行 ctx.Done() 操作, sleepRandomContext() 退出, cancelFunction() 退出, main() 退出;
  • 场景 6: sleepRandomContext 异常, 通过 defer 执行 ch <- true, doWorkContext 函数接收到 case <-ch 的信号后, doWorkContext() 退出, main() 退出;

前面 3 个是正常场景, 后面 3 个是异常场景, 无论哪种场景, 设计思路是, 当前函数退出时, 下游所有 context 需要全部关闭, 这个是依赖 context 可传递的特性, 同时也能通知上游"我已经关闭了, 请你继续你后续的操作"。

1.5. 总结

我们今天主要讨论的是 context 包中的函数和 Context 类型, 该包中的函数都是用于产生新的 Context 类型值的, Context 类型是一个可以帮助我们实现多 goroutine 协作流程的同步工具, 不但如此, 我们还可以通过此类型的值传达撤销信号或传递数据。Context 类型的实际值大体上分为三种, 即: 根 Context 值、可撤销的 Context 值和含数据的 Context 值。所有的 Context 值共同构成了一颗上下文树, 这棵树的作用域是全局的, 而根 Context 值就是这棵树的根, 它是全局唯一的, 并且不提供任何额外的功能。可撤销的 Context 值又分为: 只可手动撤销的 Context 值, 和可以定时撤销的 Context 值, 我们可以通过生成它们时得到的撤销函数来对其进行手动的撤销。对于后者, 定时撤销的时间必须在生成时就完全确定, 并且不能更改, 不过我们可以在过期时间达到之前, 对其进行手动的撤销, 一旦撤销函数被调用, 撤销信号就会立即被传达给对应的 Context 值, 并由该值的 Done 方法返回的接收通道表达出来。"撤销"这个操作是 Context 值能够协调多个 goroutine 的关键所在, 撤销信号总是会沿着上下文树叶子节点的方向传播开来。含数据的 Context 值不能被撤销, 而可撤销的 Context 值又无法携带数据, 由于它们共同组成了一个有机的整体(即上下文树), 所以在功能上要比 sync.WaitGroup 强大得多。

Golang context.Context相关推荐

  1. golang 上下文 Context

    上下文 context.Context Go 语言中用来设置截止日期.同步信号,传递请求相关值的结构体.上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们 ...

  2. 深入理解Golang之context

    深入理解Golang之context context是Go并发编程中常用到一种编程模式.本文将从为什么需要context,深入了解context的实现原理,以了解如何使用context. 作者:Tur ...

  3. Golang的context理解

    使用方法 context用于表示一个请求的上下文.一个网络请求,一般开启一个协程处理,而这个协程内部还会开启其它的协程继续处理.为了传递一个请求在不同协程中的处理情况(比如是否超时等),我们利用con ...

  4. golang:context介绍

    我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战 1 前言 最近实现系统的分布式日志与事务管理时,在寻求所谓的全局唯一Goroutine ID无果之后,决定还是简单利用Conte ...

  5. 深入解析Golang之Context

    ​context是什么 context翻译成中文就是上下文,在软件开发环境中,是指接口之间或函数调用之间,除了传递业务参数之外的额外信息,像在微服务环境中,传递追踪信息traceID, 请求接收和返回 ...

  6. Golang中context实现原理剖析

    转载: Go 并发控制context实现原理剖析 1. 前言 Golang context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生gorou ...

  7. Golang 之context用法

    文章目录 1. context 2. context.go 2.0 结构图 2.1 Context interface 2.2 emptyCtx 2.3 cancelCtx 2.4 valueCtx ...

  8. golang库context学习

    context库 context最早的背景说明还是来源于官方的 博客,说明如下: 在Go服务器中,每个传入请求都在其自己的goroutine中进行处理. 请求处理程序通常会启动其他goroutine来 ...

  9. Go context.Context的学习

    一.前言 Golang context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的gorout ...

最新文章

  1. word doc怎么显示base64图片_win10系统word文档打印不出图片怎么办
  2. vc 显示非模态对话框
  3. 为利用 QT 制作的项目设置图标
  4. ps命令与top命令参数意义详解
  5. 再谈csdn blog的bug
  6. JRE 和 JDK历史版本是为了帮助开发
  7. python turtle库有什么用_turtle库使用简介
  8. 关于ISIC数据集如何下载的问题
  9. 多种矿石混合的抗干扰矿石对讲机
  10. 调整w7计算机屏幕一直亮,win7系统电脑屏幕不休眠保持常亮状态设置的操作方法...
  11. 每个人都是雕刻自己的艺术家,生活是你的背景
  12. MySQL的基本操作(五)
  13. 看名言后的心得体会学会融会贯通
  14. CodeForces - 581B - Luxurious Houses 逆序处理水
  15. Shell脚本详解---一篇搞定
  16. 计算机网络用户名及密码如何查询,用wifi连接电脑的怎么查看宽带账号密码
  17. Atmel推出业内首款面向智能能源和自动化应用的IEEE 802.15.4g-2012双频段收发器
  18. vm虚拟机网络标志_虚拟机安装win7系统后网络图标黄色标志不能上网如何解决
  19. 【蘑菇街裁员回应】覆巢之下无完卵
  20. android砖刷机精灵,Android刷机精灵:喜刷刷

热门文章

  1. ubuntu 18.04中的shutter无法编辑截图
  2. CentOS-8操作系统
  3. 【多线程常见面试题】
  4. Discourse开源论坛搭建
  5. 【数值分析】用matlab解决插值问题、常微分方程初值问题
  6. 享受代码的快乐--小米抢购前端代码分析
  7. Linux下安装libgdal库,libjpeg库和libtiff库
  8. C语言视频教程-谭浩强版-小甲鱼主讲—P18
  9. 实时操作系统UCOS学习笔记1----UCOSII简介
  10. 微信订阅号签到功能_微信公众号积分签到功能怎么添加,怎么制作微信签到赚积分...