NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

Golang 并发编程与 Context

  • 2019-12-03
  • 本文字数:5271 字

    阅读完需:约 17 分钟

Golang 并发编程与 Context

5.1 上下文 Context

Context 是 Golang 中非常有趣的设计,它与 Go 语言中的并发编程有着比较密切的关系,在其他语言中我们很难见到类似 Context 的东西,它不仅能够用来设置截止日期、同步『信号』还能用来传递请求相关的值。


这一节就会介绍 Go 语言中这个非常常见的 Context 接口,我们将从这里开始了解 Go 语言并发编程的设计理念以及实现原理。

__1. 概述

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



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



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



这其实就是 Golang 中上下文的最大作用,在不同 Goroutine 之间对信号进行同步避免对计算资源的浪费,与此同时 Context 还能携带以请求为作用域的键值对信息。

__1.1. 接口

Context 其实是 Go 语言 context 包对外暴露的接口,该接口定义了四个需要实现的方法,其中包括:


  1. Deadline 方法需要返回当前 Context 被取消的时间,也就是完成工作的截止日期;

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

  3. Err 方法会返回当前 Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值;

  4. 如果当前 Context 被取消就会返回 Canceled 错误;

  5. 如果当前 Context 超时就会返回 DeadlineExceeded 错误;

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


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


context 包中提供的 BackgroundTODOWithDeadline 等方法就会返回实现该接口的私有结构体的,我们会在后面的小节中详细介绍它们的工作原理。

__1.2. 示例

我们可以通过一个例子简单了解一下 Context 是如何对信号进行同步的,在这段代码中我们创建了一个过期时间为 1s 的上下文,并将上下文传入 handle 方法,该方法会使用 500ms 的时间处理该『请求』:


func main() {    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)    defer cancel()
go handle(ctx, 500*time.Millisecond)
select { case <-ctx.Done(): fmt.Println("main", ctx.Err()) }}
func handle(ctx context.Context, duration time.Duration) { select { case <-ctx.Done(): fmt.Println("handle", ctx.Err())
case <-time.After(duration): fmt.Println("process request with", duration) }}
复制代码


所以我们有足够的时间处理该『请求』,而运行上述代码时会打印出如下所示的内容:


$ go run context.goprocess request with 500msmain context deadline exceeded
复制代码


『请求』被 Goroutine 正常处理没有进入超时的 select 分支,但是在 main 函数中的 select 却会等待 Context 的超时最终打印出 main context deadline exceeded,如果我们将处理『请求』的时间改成 1500ms,当前处理的过程就会因为 Context 到截止日期而被中止:


$ go run context.gomain context deadline exceededhandle context deadline exceeded
复制代码


两个函数都会因为 ctx.Done() 返回的管道被关闭而中止,也就是上下文超时。


相信这两个例子能够帮助各位读者了解 Context 的使用方法以及基本的工作原理 — 多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就停止当前正在执行的工作并提前返回。

__2. 实现原理

Context 相关的源代码都在 context.go 这个文件中,在这一节中我们就会从 Go 语言的源代码出发介绍 Context 的实现原理,包括如何在多个 Goroutine 之间同步信号、为请求设置截止日期并传递参数和信息。

__2.1. 默认上下文

context 包中,最常使用其实还是 context.Backgroundcontext.TODO 两个方法,这两个方法最终都会返回一个预先初始化好的私有变量 backgroundtodo


func Background() Context {    return background}
func TODO() Context { return todo}
复制代码


这两个变量是在包初始化时就被创建好的,它们都是通过 new(emptyCtx) 表达式初始化的指向私有结构体 emptyCtx 的指针,这是包中最简单也是最常用的类型:


type emptyCtx int
func (*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}
复制代码


它对 Context 接口方法的实现也都非常简单,无论何时调用都会返回 nil 或者空值,并没有任何特殊的功能,BackgroundTODO 方法在某种层面上看其实也只是互为别名,两者没有太大的差别,不过 context.Background() 是上下文中最顶层的默认值,所有其他的上下文都应该从 context.Background() 演化出来。



我们应该只在不确定时使用 context.TODO(),在多数情况下如果函数没有上下文作为入参,我们往往都会使用 context.Background() 作为起始的 Context 向下传递。

__2.2. 取消信号

WithCancel 方法能够从 Context 中创建出一个新的子上下文,同时还会返回用于取消该上下文的函数,也就是 CancelFunc,我们直接从 WithCancel 函数的实现来看它到底做了什么:


func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {    c := newCancelCtx(parent)    propagateCancel(parent, &c)    return &c, func() { c.cancel(true, Canceled) }}
复制代码


newCancelCtx 是包中的私有方法,它将传入的父上下文包到私有结构体 cancelCtx{Context: parent} 中,cancelCtx 就是当前函数最终会返回的结构体类型,我们在详细了解它是如何实现接口之前,先来了解一下用于传递取消信号的 propagateCancel 函数:


func propagateCancel(parent Context, child canceler) {    if parent.Done() == nil {        return // parent is never canceled    }    if p, ok := parentCancelCtx(parent); ok {        p.mu.Lock()        if p.err != nil {            child.cancel(false, p.err)        } else {            if p.children == nil {                p.children = make(map[canceler]struct{})            }            p.children[child] = struct{}{}        }        p.mu.Unlock()    } else {        go func() {            select {            case <-parent.Done():                child.cancel(false, parent.Err())            case <-child.Done():            }        }()    }}
复制代码


该函数总共会处理与父上下文相关的三种不同的情况:


  1. parent.Done() == nil,也就是 parent 不会触发取消事件时,当前函数直接返回;

  2. child 的继承链上有 parent 是可以取消的上下文时,就会判断 parent 是否已经触发了取消信号;

  3. 如果已经被取消,当前 child 就会立刻被取消;

  4. 如果没有被取消,当前 child 就会被加入 parentchildren 列表中,等待 parent 释放取消信号;

  5. 遇到其他情况就会开启一个新的 Goroutine,同时监听 parent.Done()child.Done() 两个管道并在前者结束后立刻调用 child.cancel 取消子上下文;


这个函数的主要作用就是在 parentchild 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会发生状态不一致的问题。


cancelCtx 实现的几个接口方法其实没有太多值得介绍的地方,该结构体最重要的方法其实是 cancel 方法,这个方法会关闭上下文的管道并向所有的子上下文发送取消信号:


func (c *cancelCtx) cancel(removeFromParent bool, err error) {    c.mu.Lock()    if c.err != nil {        c.mu.Unlock()        return    }    c.err = err    if c.done == nil {        c.done = closedchan    } else {        close(c.done)    }    for child := range c.children {        child.cancel(false, err)    }    c.children = nil    c.mu.Unlock()
if removeFromParent { removeChild(c.Context, c) }}
复制代码


除了 WithCancel 之外,context 包中的另外两个函数 WithDeadlineWithTimeout 也都能创建可以被取消的上下文,WithTimeout 只是 context 包为我们提供的便利方法,能让我们更方便地创建 timerCtx


func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {    return WithDeadline(parent, time.Now().Add(timeout))}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) }}
复制代码


WithDeadline 方法在创建 timerCtx 上下文的过程中,判断了上下文的截止日期与当前日期,并通过 time.AfterFunc 方法创建了定时器,当时间超过了截止日期之后就会调用 cancel 方法同步取消信号。


timerCtx 结构体内部嵌入了一个 cancelCtx 结构体,也『继承』了相关的变量和方法,除此之外,持有的定时器和 timer 和截止时间 deadline 也实现了定时取消这一功能:


type timerCtx struct {    cancelCtx    timer *time.Timer // Under cancelCtx.mu.
deadline time.Time}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true}
func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) if removeFromParent { removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock()}
复制代码


cancel 方法不仅调用了内部嵌入的 cancelCtx.cancel,还会停止持有的定时器减少不必要的资源浪费。

__2.3. 传值方法

在最后我们需要了解一下如何使用上下文传值,context 包中的 WithValue 函数能从父上下文中创建一个子上下文,传值的子上下文使用私有结构体 valueCtx 类型:


func WithValue(parent Context, key, val interface{}) Context {    if key == nil {        panic("nil key")    }    if !reflectlite.TypeOf(key).Comparable() {        panic("key is not comparable")    }    return &valueCtx{parent, key, val}}
复制代码


valueCtx 函数会将除了 Value 之外的 ErrDeadline 等方法代理到父上下文中,只会处理 Value 方法的调用,然而每一个 valueCtx 内部也并没有存储一个键值对的哈希,而是只包含一个键值对:


type valueCtx struct {    Context    key, val interface{}}
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
复制代码


如果当前 valueCtx 中存储的键与 Value 方法中传入的不匹配,就会从父上下文中查找该键对应的值直到在某个父上下文中返回 nil 或者查找到对应的值。

__3. 总结

Go 语言中的 Context 的主要作用还是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费,虽然传值也是它的功能之一,但是这个功能我们还是很少用到。


在真正使用传值的功能时我们也应该非常谨慎,不能将请求的所有参数都使用 Context 进行传递,这是一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

__4. Reference

__5. 其他

__5.1. 关于图片和转载

**本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/golang/concurrency/golang-context.html


2019-12-03 15:101053

评论

发布
暂无评论
发现更多内容

PVP2多屏幕演示投放软件:PVP2 ProVideoPlayer2 中文版

真大的脸盆

Mac Mac 软件 视频播放器 视频播放

在 Kubernetes 中实施零信任的七条准则

NGINX开源社区

nginx Kubernetes

软件测试/测试开发丨Linux 常用高频命令

测试人

Linux 软件测试 自动化测试 测试开发

用友自主研发企业商用版TimensionDB时序数据库重磅发布!

用友BIP

数据库 用友iuap 用友技术大会 升级企业数智化底座

为什么老有人想让我们“程序员”失业? | 社区征文

不叫猫先生

人工智能 程序人生 ChatGPT 三周年征文

智汇昌平,数赢未来——宝德京产自主创新服务器正式下线

Geek_2d6073

有奖征文丨【玩转Cloud Studio】第二季来啦!

CODING DevOps

Cloud Studio 云端IDE 在线编程 有奖征文 活动推荐

人工智能训练数据集:基础与发展

来自四九城儿

升级企业数智化底座是数智化2.0阶段的“最优解”

用友BIP

用友iuap 用友技术大会 数智化底座 数智化2.0阶段

“前端已死”还是“娱乐至死”?做个清醒的前端

这我可不懂

前端 低代码

人工智能时代来临,殊不知低代码早已出手

加入高科技仿生人

人工智能 低代码 数智化 数智融合

阿里内部微服务架构秘籍:SpringCloudAlibaba全彩版笔记开源

采菊东篱下

编程 微服务

攻防大牛在身边,2023首届阿里云CTF 大赛冠军揭晓

Lily

Wallys/QSDK/IPQ4019 and IPQ4029 chipsets support 20 km remote transmission

Cindy-wallys

IPQ4019 ipq4029

设计模式天花板,详解23种设计模式+7大设计原则

小小怪下士

Java 程序员 设计模式

软件测试/测试开发丨uiautomator2 自动化测试工具使用

测试人

软件测试 自动化测试 测试开发 uiautomator

低代码是开发的未来,还是只能解决边角问题的鸡肋?

引迈信息

前端 后端 低代码 JNPF

百度与用友网络签署战略合作

百度开发者中心

智能制造 文心一言

作为前端你还不懂MutationObserver?那Out了

不叫猫先生

JavaScript 前端 三周年连更 MutationObserver

大淘宝技术斩获NTIRE 2023视频质量评价比赛冠军(内含夺冠方案)

阿里巴巴大淘宝技术

视频 NTIRE

从“捐赠”到“接受捐赠”,这背后是openEuler的两次蜕变

Geek_2d6073

耗时72天!终于把GitHub上热度最高的Java面试八股文整理出来了,涵盖多家大厂面试真题

架构师之道

Java 面试

法大大发布数智化签约管理平台,赋能企业高效增长

人称T客

Cloud Studio 一个好用的在线编程工具

CODING DevOps

开发 部署 Cloud Studio 云端IDE 在线编程

招商基金数字化转型下的研发管理|标杆案例

万事ONES

测试Java初学者建议

FunTester

iOS MachineLearning 系列(5)—— 视频中的物体运动追踪

珲少

少年与阿童木:一场软件竞技赛背后的智能未来

脑极体

机器人 华为云

极客时间「大师课·深度剖析 RocketMQ5.0」上线啦,欢迎免费领取!

Apache RocketMQ

云原生 消息队列

人脸识别:城市公共交通

百度开发者中心

人工智能 人脸识别

ThingsBoard 前端项目内置部件开发

echeverra

thingsboard

Golang 并发编程与 Context_文化 & 方法_Draveness_InfoQ精选文章