Go context.Context的学习

news2024/11/24 22:46:08

一、前言

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

context翻译成中文是”上下文”,即它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。

典型的使用场景如下图所示:
在这里插入图片描述
上图中由于goroutine派生出子goroutine,而子goroutine又继续派生新的goroutine,这种情况下使用WaitGroup就不太容易,因为子goroutine个数不容易确定。而使用context就可以很容易实现。

二、实现原理

context实际上只定义了接口,凡是实现该接口的类都可称为是一种context,官方包中实现了几个常用的context,分别可用于不同的场景。

2.1 接口定义

源码包中src/context/context.go:Context 定义了该接口:

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

基础的context接口只定义了4个方法,下面分别简要说明一下:

2.1.1 Deadline()

该方法返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,则ok == false,此时deadline为一个初始值的time.Time值

2.1.2 Done()

该方法返回一个channel,需要在select-case语句中使用,如”case <-context.Done():”。

  • 当context关闭后,Done()返回一个被关闭的管道,关闭的管道仍然是可读的,据此goroutine可以收到关闭请求;
  • 当context还未关闭时,Done()返回nil。

2.1.3 Err()

该方法描述context关闭的原因。关闭原因由context实现控制,不需要用户设置。比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同:

  • 因deadline关闭:“context deadline exceeded”;
  • 因主动关闭: “context canceled”。

当context关闭后,Err()返回context的关闭原因;
当context还未关闭时,Err()返回nil;

2.1.4 Value()

有一种context,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息。
Value()方法就是用于此种类型的context,该方法根据key值查询map中的value。具体使用后面示例说明。

2.2 空context

context包中定义了一个空的context, 名为emptyCtx,用于context的根节点,空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节点。

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包中定义了一个公用的emptCtx全局变量,名为background,可以使用context.Background()获取它,实现代码如下所示:

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

context包提供了4个方法创建不同类型的context,使用这四个方法时如果没有父context,都需要传入backgroud,即backgroud作为其父节点:

  • WithCancel()
  • WithDeadline()
  • WithTimeout()
  • WithValue()

context包中实现Context接口的struct,除了emptyCtx外,还有cancelCtx、timerCtx和valueCtx三种,正是基于这三种context实例,实现了上述4种类型的context。

context包中各context类型之间的关系,如下图所示:
在这里插入图片描述
struct cancelCtx、timerCtx、valueCtx都继承于Context,下面分别介绍这三个struct。

2.3 cancelCtx

源码包中src/context/context.go:cancelCtx 定义了该类型context:

type cancelCtx struct {
    Context
    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

children中记录了由此context派生的所有child,此context被cancel时会把其中的所有child都cancel掉。

cancelCtx与deadline和value无关,所以只需要实现Done()和Err()外露接口即可。

2.3.1 Done()接口实现

按照Context定义,Done()接口只需要返回一个channel即可,对于cancelCtx来说只需要返回成员变量done即可。

这里直接看下源码,非常简单:

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

由于cancelCtx没有指定初始化函数,所以cancelCtx.done可能还未分配,所以需要考虑初始化。
cancelCtx.done会在context被cancel时关闭,所以cancelCtx.done的值一般经历如下三个阶段:
nil –> chan struct{} –> closed chan。

2.3.2 Err()接口实现

按照Context定义,Err()只需要返回一个error告知context被关闭的原因。对于cancelCtx来说只需要返回成员变量err即可。还是直接看下源码:

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

cancelCtx.err默认是nil,在context被cancel时指定一个error变量: var Canceled = errors.New(“context canceled”)。

2.3.3 cancel()接口实现

cancel()内部方法是理解cancelCtx的最关键的方法,其作用是关闭自己和其后代,其后代存储在cancelCtx.children的map中,其中key值即后代对象,value值并没有意义,这里使用map只是为了方便查询而已。

cancel方法实现伪代码如下所示:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    c.mu.Lock()

    c.err = err                          //设置一个error,说明关闭原因
    close(c.done)                     //将channel关闭,以此通知派生的context

    for child := range c.children {   //遍历所有children,逐个调用cancel方法
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {            //正常情况下,需要将自己从parent删除
        removeChild(c.Context, c)
    }
}

实际上,WithCancel()返回的第二个用于cancel context的方法正是此cancel()。

2.3.4 WithCancel()方法实现

WithCancel()方法作了三件事:

  • 初始化一个cancelCtx实例
  • 将cancelCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
  • 返回cancelCtx实例和cancel()方法

其实现源码如下所示:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)   //将自身添加到父节点
    return &c, func() { c.cancel(true, Canceled) }
}

这里将自身添加到父节点的过程有必要简单说明一下:

  1. 如果父节点也支持cancel,也就是说其父节点肯定有children成员,那么把新context添加到children里即可;
  2. 如果父节点不支持cancel,就继续向上查询,直到找到一个支持cancel的节点,把新context添加到children里;
  3. 如果所有的父节点均不支持cancel,则启动一个协程等待父节点结束,然后再把当前context结束。

2.3.5 典型使用案例

一个典型的使用cancel context的例子如下所示:

package main

import (
    "fmt"
    "time"
    "context"
)

func HandelRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running")
            time.Sleep(2 * time.Second)
        }
    }
}

func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running")
            time.Sleep(2 * time.Second)
        }
    }
}

func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running")
            time.Sleep(2 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go HandelRequest(ctx)

    time.Sleep(5 * time.Second)
    fmt.Println("It's time to stop all sub goroutines!")
    cancel()

    //Just for test whether sub goroutines exit or not
    time.Sleep(5 * time.Second)
}

上面代码中协程HandelRequest()用于处理某个请求,其又会创建两个协程:WriteRedis()、WriteDatabase(),main协程创建context,并把context在各子协程间传递,main协程在适当的时机可以cancel掉所有子协程。
程序输出如下所示:

HandelRequest running
WriteDatabase running
WriteRedis running
HandelRequest running
WriteDatabase running
WriteRedis running
HandelRequest running
WriteDatabase running
WriteRedis running
It's time to stop all sub goroutines!
WriteDatabase Done.
HandelRequest Done.
WriteRedis Done.

2.4 timerCtx

源码包中src/context/context.go:timerCtx 定义了该类型context:

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

timerCtx在cancelCtx基础上增加了deadline用于标示自动cancel的最终时间,而timer就是一个触发自动cancel的定时器。

由此,衍生出WithDeadline()和WithTimeout()。实现上这两种类型实现原理一样,只不过使用语境不一样:

  • deadline: 指定最后期限,比如context将2018.10.20 00:00:00之时自动结束
  • timeout: 指定最长存活时间,比如context将在30s后结束。

对于接口来说,timerCtx在cancelCtx基础上还需要实现Deadline()和cancel()方法,其中cancel()方法是重写的。

2.4.1 Deadline()接口实现

Deadline()方法仅仅是返回timerCtx.deadline而矣。而timerCtx.deadline是WithDeadline()或WithTimeout()方法设置的。

2.4.2 cancel()接口实现

cancel()方法基本继承cancelCtx,只需要额外把timer关闭。
timerCtx被关闭后,timerCtx.cancelCtx.err将会存储关闭原因:

  • 如果deadline到来之前手动关闭,则关闭原因与cancelCtx显示一致;
  • 如果deadline到来时自动关闭,则原因为:”context deadline exceeded”

2.4.3 WithDeadline()方法实现

WithDeadline()方法实现步骤如下:

  • 初始化一个timerCtx实例
  • 将timerCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
  • 启动定时器,定时器到期后会自动cancel本context
  • 返回timerCtx实例和cancel()方法

也就是说,timerCtx类型的context不仅支持手动cancel,也会在定时器到来后自动cancel。

2.4.4 WithTimeout()方法实现

WithTimeout()实际调用了WithDeadline,二者实现原理一致。
看代码会非常清晰:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

2.4.5 典型使用案例

下面例子中使用WithTimeout()获得一个context并在其子协程中传递:

package main

import (
    "fmt"
    "time"
    "context"
)

func HandelRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running")
            time.Sleep(2 * time.Second)
        }
    }
}

func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running")
            time.Sleep(2 * time.Second)
        }
    }
}

func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running")
            time.Sleep(2 * time.Second)
        }
    }
}

func main() {
    ctx, _ := context.WithTimeout(context.Background(), 5 * time.Second)
    go HandelRequest(ctx)

    time.Sleep(10 * time.Second)
}

主协程中创建一个10s超时的context,并将其传递给子协程,10s自动关闭context。程序输出如下:

HandelRequest running
WriteRedis running
WriteDatabase running
HandelRequest running
WriteRedis running
WriteDatabase running
HandelRequest running
WriteRedis running
WriteDatabase running
HandelRequest Done.
WriteDatabase Done.
WriteRedis Done.

2.5 valueCtx

源码包中src/context/context.go:valueCtx 定义了该类型context:

type valueCtx struct {
    Context
    key, val interface{}
}

valueCtx只是在Context基础上增加了一个key-value对,用于在各级协程间传递一些数据。
由于valueCtx既不需要cancel,也不需要deadline,那么只需要实现Value()接口即可。

2.5.1 Value()接口实现

由valueCtx数据结构定义可见,valueCtx.key和valueCtx.val分别代表其key和value值。 实现也很简单:

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

这里有个细节需要关注一下,即当前context查找不到key时,会向父节点查找,如果查询不到则最终返回interface{}。也就是说,可以通过子context查询到父的value值。

2.5.2 WithValue()方法实现

WithValue()实现也是非常的简单, 伪代码如下:

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    return &valueCtx{parent, key, val}
}

2.5.3 典型使用案例

下面示例程序展示valueCtx的用法:

package main

import (
    "fmt"
    "time"
    "context"
)

func HandelRequest(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running, parameter: ", ctx.Value("parameter"))
            time.Sleep(2 * time.Second)
        }
    }
}

func main() {
    ctx := context.WithValue(context.Background(), "parameter", "1")
    go HandelRequest(ctx)

    time.Sleep(10 * time.Second)
}

上例main()中通过WithValue()方法获得一个context,需要指定一个父context、key和value。然后通将该context传递给子协程HandelRequest,子协程可以读取到context的key-value。

注意:本例中子协程无法自动结束,因为context是不支持cancle的,也就是说<-ctx.Done()永远无法返回。如果需要返回,需要在创建context时指定一个可以cancel的context作为父节点,使用父节点的cancel()在适当的时机结束整个context。

三、总结

  • Context仅仅是一个接口定义,根据实现的不同,可以衍生出不同的context类型;
  • cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
  • timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;
  • valueCtx实现了Context接口,通过WithValue()创建valueCtx实例;

三种context实例可互为父节点,从而可以组合成不同的应用形式;

参考材料

  1. context—《Go专家编程》
  2. pkg/context—《Go深入浅出》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/80003.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java计算机毕业设计ssm疫情期间校园车辆入校预约管理服务系统1171a(附源码、数据库)

java计算机毕业设计ssm疫情期间校园车辆入校预约管理服务系统1171a&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe…

没有二十年功力,写不出 Thread.sleep(0) 这一行“看似无用”的代码

这篇文章要从一个奇怪的注释说起&#xff0c;就是下面这张图&#xff1a; 我们可以不用管具体的代码逻辑&#xff0c;只是单单看这个 for 循环。 在循环里面&#xff0c;专门有个变量 j&#xff0c;来记录当前循环次数。 第一次循环以及往后每 1000 次循环之后&#xff0c;进…

ssm+vue基本微信小程序的校园二手商城系统 计算机毕业设计

在当今社会的高速发展过程中&#xff0c;产生的劳动力越来越大&#xff0c;提高人们的生活水平和质量&#xff0c;尤其计算机科技的进步&#xff0c;数据和信息以人兴化为本的目的&#xff0c;给人们提供优质的服务&#xff0c;其中网上购买二手商品尤其突出&#xff0c;使我们…

211大数据专业大四学生,放弃字节转正,选择老家大型国企,听听他怎么说?...

点击上方 "大数据肌肉猿"关注, 星标一起成长点击下方链接&#xff0c;进入高质量学习交流群今日更新| 1052个转型案例分享-大数据交流群分享学习群一位大数据专业同学的秋招学习和求职经历&#xff0c;他是211大四学生&#xff0c;年初才开始学习&#xff0c;但还好赶…

181.基于Django的云文件存储使用方式——七牛云存储

1.文件云存储 1.1 概述 在Django项目中&#xff0c;用户上传的文件以及项目中使用的静态文件&#xff0c;默认读书存储本地&#xff0c;保存在服务器中&#xff0c;但是&#xff0c;其实我们也可以将他们保存在云存储中&#xff0c;譬如七牛云存储、阿里云存储、亚马逊云存储…

【网络安全】提防黑客来“敲门”

前言 互联网在给我们带来便捷高效的同时&#xff0c;也给一些不法分子提供了可乘之机。网络诈骗、窃取个人信息等花样层出不穷&#xff0c;骚扰电话、垃圾短信扰乱着我们的正常生活&#xff0c;使网络空间抹上一笔灰色。网络安全与每个人都息息相关&#xff0c;所以我们必须理…

Python测试进阶(三)

文章目录性能测试JMeter测试计划模拟并发结果分析分布式性能监控grafanaFluxPrometheus小结性能测试 为什么做性能测试&#xff1f;主要是解决这些问题 什么是性能测试 模拟多个用户的操作&#xff0c;看对服务器性能的影响 指标 TPS&#xff1a;transaction per secondRT&…

基于Kubeadm快速部署一个K8s集群

目录kubeadm概述安装要求准备环境安装kubelet、kubeadm、kubectl使用kubeadm引导集群下载各个机器需要的镜像初始化主节点安装网络组件常用shell命令测试kubernetes集群部署dashboardkubeadm概述 kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具。 这个工具能通…

索引创建、删除的sql语句

目录 创建索引 使用ALTER TABLE 语句创建索引 使用CREATE TABLE 语句创建索引 删除索引 使用ALTER TABLE 语句删除索引 使用DROP INDEX 语句删除索引 创建索引 1、创建表的同时&#xff0c;指定给某个字段创建索引&#xff08;name&#xff09; create table cat(id …

SaaS 产品的文档策略

作者&#xff1a;Vaijayanti Nerkar 和 Priya Shetye&#xff0c;BMC Software 几年前&#xff0c;当 BMC Software 决定进军 SaaS 市场时&#xff0c;该公司开始投资开发基于云的产品。20多年来&#xff0c;BMC Software 产品都是典型的本地产品&#xff0c;因此&#xff0c;…

2022.12.11-YOLOv5使用NCNN将模型部署到Android端教程(1)部署自己的训练模型到Android实现静态图片检测

文章目录1. 前言2. 模型转换2.1. NCNN2.1.1. 简介2.1.2. ncnn2.1.3. ncnn-android-yolov52.2. 项目准备2.2.1. 安装Android studio2.2.2. 下载解压源码2.3. 安卓源码重新编译2.3.1. 构建工程2.3.2. 修改源码2.3.2.1. 修改CMakeLists.txt中的路径2.3.2.2. 重新重新ysnc project2…

【华为上机真题 2022】相对开音节

&#x1f388; 作者&#xff1a;Linux猿 &#x1f388; 简介&#xff1a;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;Linux、C/C、云计算、物联网、面试、刷题、算法尽管咨询我&#xff0c;关注我&#xff0c;有问题私聊&#xff01; &…

【无需注册账号】只需两步, Ai Studio上也可以玩[ChatGPT]了

☆ 只需两步&#xff0c; Ai Studio上也可以玩[ChatGPT]了 ☆ 无需账号即可体验喽~ 体验地址 只需两步&#xff0c; Ai Studio上也可以玩[ChatGPT]了 文章目录☆ 只需两步&#xff0c; Ai Studio上也可以玩[ChatGPT]了 ☆0 Fork后进入项目应用中心体验☆本页面下方体验☆注意…

5G无线技术基础自学系列 | 勘测准备

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 基站在详细的勘测之前需要做的准备包括…

CN_UDP协议

文章目录UDP协议UDP概述UDP的首部格式UDP数据报封装入IP数据报UDP校验伪首部真首部UDP数据报处理例UDP vs TCPUDP协议 User Datagram Protocol - Wikipedia 1Attributes2Ports3UDP datagram structure4Checksum computation 4.1IPv4 pseudo header4.2IPv6 pseudo header 5Reli…

百度安全查询,查询网址是否存在百度安全风险的方法

如果网站被百度安全识别为风险网站&#xff0c;或者是提示该页面可能存在虚假信息&#xff0c;该页面可能已被非法篡改&#xff0c;那么就不妙了。 怎样才能知道自己的网是否存在百度安全风险&#xff1f; 查询网站百度安全的方法: 第一步、打开SEO综合查询工具 第二步、添加…

[附源码]Python计算机毕业设计SSM基于的校园失物招领平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

线程池 (通俗易懂)

线程池一、线程池是什么二、标准库中的线程池三、实现线程池3.1 思路与细节3.2 完整代码四、标准库里的构造方法一、线程池是什么 之前我们已经认识过"池"&#xff1a;String&#xff0c;字符串常量池&#xff1b;MySQL JDBC&#xff0c;数据库连接池(DataSource)……

【案例实践】EKMA曲线及大气O3来源解析

【查看原文】EKMA曲线及大气O3来源解析实践技术应用 目前&#xff0c;大气臭氧污染成为我国“十四五”期间亟待解决的环境问题。臭氧污染不仅对气候有重要影响&#xff0c;而且对人体健康、植物生长均有严重损害。为了高效、精准地治理区域大气臭氧污染&#xff0c;首先需要了…

【OpenCV学习】第14课:边缘检测与自定义线性滤波(卷积, Rebert算子, Sobel算子, 拉普拉斯算子)

仅自学做笔记用,后续有错误会更改 参考文章&#xff1a;http://t.zoukankan.com/whw1314-p-12007928.html 理论 卷积的概念&#xff1a; 在图像上使用卷积的目的&#xff1a;模糊图像&#xff0c; 提取边缘轮廓&#xff0c; 图像锐化等 卷积如何工作&#xff1a; 下边给出…