速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

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:101185

评论

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

如何避免写重复代码:善用抽象和组合

阿里技术

Java 代码实战

Mysql DDL执行方式-pt-osc介绍 | 京东云技术团队

京东科技开发者

MySQL 数据库 企业号 5 月 PK 榜 DDL执行方式 pt-soc

莉莉丝游戏与火山引擎ByteHouse达成合作,为实时数仓建设提速

字节跳动数据平台

数据仓库 云原生 实时

Kafka集群是如何选择leader,你知道吗?

做梦都在改BUG

Java kafka 集群

Flutter三棵树系列之BuildOwner | 京东云技术团队

京东科技开发者

flutter 移动开发 源码解读 企业号 5 月 PK 榜 BuildOwner

全国流体力学盛会召开,飞桨AI4S携最新科研进展亮相西湖大学

飞桨PaddlePaddle

人工智能 百度飞桨 科学计算

辅助测试和研发人员的一款小插件【数据安全】 | 京东云技术团队

京东科技开发者

浏览器 数据安全 插件开发 企业号 5 月 PK 榜

电商行业实践专栏上线|阿里巴巴风控实战如何解决大规模风控的技术难点?

Apache Flink

大数据 flink 实时计算

阿里P8大佬的1800页计算机基础知识总结与操作系统,太强了!

做梦都在改BUG

Java 程序员 操作系统

kafka集群是如何选择leader,你知道吗?

JAVA旭阳

kafka

Git入门指南:从新手到高手的完全指南

小万哥

git Linux 程序员 后端 C/C++

国内好用的堡垒机推荐-行云管家堡垒机

行云管家

网络安全 堡垒机

小程序容器与PWA的完美结合:提升应用性能与用户体验

FinFish

私有小程序技术 小程序容器 PWA 小程序化 小程序技术

「2023最新版」Java基础、中级、高级面试题总结(1000道题含答案解析)

采菊东篱下

java面试

牛客网 2023 最新 1100道 Java 面试题来袭,面面俱到,太全了!

架构师之道

java面试

如何将千亿文件放进一个文件系统,EuroSys'23 CFS 论文背后的故事

百度Geek说

数据库 云计算 百度 企业号 5 月 PK 榜

双非渣硕,开发两年,苦刷算法47天,四面字节斩获offer

做梦都在改BUG

Java 数据结构 算法 LeetCode

Spring Boot实现第一次启动时自动初始化数据库

做梦都在改BUG

Java spring Spring Boot

房地产行业IT运维安全就用行云管家堡垒机!

行云管家

运维 房地产 IT运维

从7天到1天,Kyligence 和亚马逊云科技助力欣和提高数据应用价值

Kyligence

数字化转型 指标平台

软件测试/测试开发丨学习笔记之Web自动化测试

测试人

程序员 软件测试 自动化测试 测试开发

MatrixOne 助力开启分布式计算格局新征程

MatrixOrigin

分布式数据库 HTAP MatrixOrigin MatrixOne 矩阵起源

阿里大神级Elasticsearch学习笔记,还学不会就埋了

做梦都在改BUG

Java elasticsearch 分布式搜索引擎 ES

医疗领域实体抽取:UIE Slim最新升级版含数据标注、serving部署、模型蒸馏等教学,助力工业应用场景快速落地

汀丶人工智能

人工智能 自然语言处理 知识图谱 关系抽取 命名实体识别

内核调试环境搭建

郑州埃文科技

网络安全 网络环境

精准快速搜索文件:Find Any File 激活版

真大的脸盆

Mac 办公效率 文件搜索 搜索工具 搜索文件

Hybrid Shuffle 测试分析和使用建议

Apache Flink

大数据 flink 实时计算

大语言模型技术原理

NineData

AIGC ChatGPT AI大语言模型 大语言模型 技术原理

ByConity与主流开源OLAP引擎(Clickhouse、Doris、Presto)性能对比分析

墨天轮

数据库 字节跳动 OLAP Clickhouse Doris

Solaris Network:BSC上首个链上合成资产解决方案

大瞿科技

太赞了,京东研发一哥力荐的高可用网站构建技术

做梦都在改BUG

Java 架构 京东

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