【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

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

评论

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

微软推出统一 Copilot AI 助手;谷歌拟自主研发 AI 芯片;苹果拟在国内引进新的 OLED 供应商丨RTE开发者日报 Vol.55

声网

如何保障汽车嵌入式软件的质量与安全?您需要了解ASPICE标准

龙智—DevSecOps解决方案

汽车软件开发 汽车软件 ASPICE OEM

海外直播、聊天交友APP的开发

西安链酷科技

直播软件开发 直播平台开发

直播系统开发|直播APP源码开发搭建的开发

西安链酷科技

语音直播平台开发 直播APP开发

im即时通讯 加密通讯设计聊天区块链系统搭建开发

西安链酷科技

直播软件开发

凹凸贴图和法线贴图的区别

3D建模设计

纹理处理 材质贴图 凹凸贴图 法线贴图

法线贴图的视线原理

3D建模设计

纹理处理 材质贴图 法线贴图

软件测试/测试开发丨利用人工智能ChatGPT完善个人简历

测试人

人工智能 程序员 软件测试 简历 ChatGPT

香港站群多IP服务器,扩展您的在线业务,全球影响力加倍

一只扑棱蛾子

站群服务器

什么是LED贴膜屏?

Dylan

技术 广告 3D LED显示屏

流行的机器学习算法——线性回归

小魏写代码

Flink Batch SQL Improvements on Lakehouse

Apache Flink

大数据 flink 实时计算

网络爬虫使用什么特点的http代理?

巨量HTTP

http代理

OpenHarmony应用开发—ArkUI组件集合

OpenHarmony开发者

OpenHarmony

龙蜥白皮书精选:机密计算平台技术

OpenAnolis小助手

开源 操作系统 白皮书 龙蜥社区 机密计算

对话在行人|承德水务:通过数智化实现“一滴水的闭环管理”

用友BIP

对话在行人

阻碍财务共享助力财务转型的三个坑,要绕行!

用友BIP

财务共享

多元变现产品全新升级,百度内容变现新解法

彭飞

TiDB binlog故障处理之drainer周期性罢工

TiDB 社区干货传送门

故障排查/诊断

【ODPS新品发布第2期】实时数仓Hologres:推出计算组实例/支持JSON数据/向量计算+大模型等新能力

阿里云大数据AI技术

大数据 阿里云

数据驱动,智能运营:2023全球商业创新大会精要

用友BIP

2023全球商业创新大会

以创新抵御经济环境压力:自动化测试ReadyAPI帮助企业平衡软件质量与成本控制

龙智—DevSecOps解决方案

自动化测试

深入浅出线程池 | 京东云技术团队

京东科技开发者

Java 线程 线程池 企业号9月PK榜

直播软件开发源码——直播系统源码

西安链酷科技

直播软件开发

海外版视频直播平台开发搭建

西安链酷科技

直播软件开发

修复 Spring Boot Actuator 未授权访问问题的指南

Apifox

Java Spring Boot 后端 Spring Boot Actuator 授权访问

NSDT孪生场景编辑器系统介绍

3D建模设计

数字孪生 建模 模拟仿真

积分商城系统软件开发

西安链酷科技

积分商城

位移贴图的实现原理

3D建模设计

纹理处理 材质贴图 位移贴图

软件测试/测试开发丨利用人工智能ChatGPT编写晋级报告

测试人

人工智能 程序员 软件测试 ChatGPT 晋级报告

基于Java+vue开发的企事业移动培训考试平台

金陵老街

java 架构

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