写点什么

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

评论

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

AI简报:图像超分模型 EDSR+RDN

AIWeker

人工智能 5月月更 超分 AI简报

移动端异构运算技术-GPU OpenCL 编程(基础篇)

百度Geek说

后端

【FinClip 黑客马拉松优秀参赛项目@灰鲸疫情地图】疫情之下,安全出行的一站式解决方案

FinClip

finclip FinClip Hackthon 疫情地图

用更云原生的方式做诊断|大规模 K8s 集群诊断利器深度解析

尔达Erda

程序员 运维 云原生 k8s 工具

typeof和instanceof

源字节1号

「深度」国产GPU的“成色”与“现实”

Finovy Cloud

服务器 GPU服务器

当开放服务网格 OSM 遇到 Pipy

Flomesh

Service Mesh 服务网格 SMI OSM

首届 FinClip Hackathon 圆满落幕,亚马逊云科技|云服务平台助力获奖团队高速成长

FinClip

亚马逊 finclip FinClip Hackthon

设计模式关系图(全网首发)之行为模式

设计模式

Clickhouse:日志表占用大量磁盘空间怎么办?

Ken

Clickhouse 日志表

零元尝鲜|阿里云 Grafana 商业化服务正式发布

阿里巴巴云原生

阿里云 云原生 Grafana 可观测工具

JSON数据传输大法第一式——用OADate处理日期格式

葡萄城技术团队

json 前端 表格开发 表格 表格技术

FLASK项目动态更新配置的方法

西北望高楼

Python flask

Squids DBMotion数据库迁移服务上线|助力云端数据畅游无阻!

沃趣科技

用了Scrum越来越累?这三点帮你走出困境

华为云开发者联盟

Scrum 团队 估算 计划会议 需求梳理

Android技术分享| Context浅析

anyRTC开发者

android 音视频 WebRTC Context 移动开发

重磅官宣!摩尔线程携手7家核心分销商伙伴共拓GPU市场机遇

科技热闻

改善CRM系统策略的方法

低代码小观

CRM 客户关系管理 企业管理系统 CRM系统 客户关系管理系统

fastposter v2.8.1 发布 电商海报生成器

物有本末

Python Pillow fastposter fast-poster

透过实例demo带你认识gRPC

华为云开发者联盟

spring gRPC 数据编码 请求映射

有奖调研!第四期(2021-2022)传统行业云原生技术落地调研——央国企篇

York

容器 DevOps 微服务 云原生

谢孟军:中国 Go 语言领军人的创业第五年

腾源会

开源 Go 语言 腾源会 WeOpen Talk

堡垒机属于什么设备?是网络设备吗?

行云管家

网络安全 服务器 堡垒机 网络设备

带你聚焦GaussDB(DWS)存储时游标使用

华为云开发者联盟

sql 数据处理 GaussDB(DWS) 游标 动态游标

茶饮市场当前情况如何?走入下半场的现制茶战局,又将去向何方?

易观分析

茶饮

云计算和运维工程师冲突吗?运维岗需要云计算知识吗?

行云管家

云计算 运维 IT运维 云运维

《方博碳讨室》:四问欧洲绿色能源转型

Geek_2d6073

天才制造者:独行侠、科技巨头和AI |深度学习崛起十年

OneFlow

人工智能 神经网络 深度学习 DeepMind 开源公司

前端路由工作原理与使用

CRMEB

深入微服务-服务注册与发现 SpringCloud Eureka之基础

janyxe

微服务 云原生 SpringCloud Eureka 服务注册与发现

时间堆原理详解及C++11的实现

C++后台开发

后端开发 Linux服务器开发 C++11 C++后台开发 时间堆

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