【Go 并发控制】上下文 context 源码

Context

在 Go 服务中,往往由一个独立的 goroutine 去处理一次请求,但在这个 goroutine 中,可能会开启别的 goroutine 去执行一些具体的事务,如数据库,RPC 等,同时,这一组 goroutine 可能还需要共同访问一些特殊的值,如用户 token, 请求过期时间等,当一个请求超时后,我们希望与此请求有关的所有 goroutine 都能快速退出,以回收系统资源。

context 包由谷歌开源,在 Go 1.7 时加入标准库,使用它可以很容易的把特定的值,取消信号, 截止日期传递给请求所涉及的所有 goroutine。

context 包的核心是 Context 接口,其结构如下:

type Context interface {Done() <-chan struct{}Err() errorDeadline() (deadline time.Time, ok bool)Value(key interface{}) interface{}
}
  1. Done 返回一个 chan, 表示一个取消信号,当这个通道被关闭时,函数应该立刻结束工作并返回。
  2. Err() 返回一个 error, 表示取消上下文的原因
  3. Deadline 会返回上下文取消的时间
  4. Value 用于从上下文中获取 key 对应的值

使用

传递取消信号(cancelation signals)

正如使用 chan 控制并发一样,我们希望传递给 goroutine 一个信号,一旦接收到这个信号,就立刻停止工作并返回,context 包提供了一个 WithCancel(), 使用它可以很方便的传递取消信号。

func useContext(ctx context.Context, id int) {for {select {case <- ctx.Done():fmt.Println("stop", id)returndefault:run(id)}}
}func G2(ctx context.Context) {nCtx, nStop := context.WithCancel(ctx)go G4(nCtx)for {select {case <- ctx.Done():fmt.Println("stop 2")nStop()returndefault:run(2)}}
}func G3(ctx context.Context) {useContext(ctx, 3)
}func G4(ctx context.Context) {useContext(ctx, 4)
}func main() {ctx, done := context.WithCancel(context.Background())go G2(ctx)go G3(ctx)time.Sleep(5*time.Second)done()time.Sleep(5*time.Second)
}

设置截止时间

func G6(ctx context.Context) {for  {select {case <- ctx.Done():t, _ := ctx.Deadline()fmt.Printf("[*] %v done: %v\n", t, ctx.Err())returndefault:fmt.Println("[#] run ...")}}
}func main() {// ctx, done := context.WithTimeout(context.Background(), time.Second * 2)ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)go G6(ctx)//done()time.Sleep(10*time.Second)
}[#] run ...
...
[*] 2020-10-31 20:24:42.0581352 +0800 CST m=+2.008975001 done: context deadline exceeded

传值

func G7(ctx context.Context) {for  {select {case <- ctx.Done():fmt.Println("cancel", ctx.Value("key"))returndefault:fmt.Println("running ", ctx.Value("key"))time.Sleep(time.Second)}}
}func main() {ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)ctx =  context2.WithValue(ctx, "key", "value")go G7(ctx)time.Sleep(10*time.Second)
}

context 包概览

context 包的核心是 context.Context 接口,另外有四个 struct 实现了 Context 接口,分别是 emptyCtx, cancelCtx, timerCtx, valueCtx, 其中 emptyCtx 是一个默认的空结构体,其余三个都是在其基础上添加了各自功能的实现,针对 emptyCtx ,context 包中暴露了两个方法 Background()TODO() 去创建一个空的 emptyCtx, 而针对后面三种具体的 struct ,context 包总共暴露了四个方法去产生对应的 struct, 他们分别是: WithCancel(), WithDeadLine(), WithTimeout(), WithValue(),对应关系如下:

TODO 和 Background

TODO 和 Background 方法用来返回一个 emptyCtx 类型,他们在实现上都一样:

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

这两个方法都会返回一个非空的上下文 emptyCtx,他永远不会被取消,用于传递给其他方法去构建更加复杂的上下文对象,一般默认使用 Background(), 只有在不确定时使用TODO(), 但实际上他们只是名字不同而已。

下面是 emptyCtx 的实现,他确实没做任何事。

type emptyCtx intfunc (*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
}

WithCancel

type cancelCtx struct {Contextmu       sync.Mutex            // 用于同步done     chan struct{}         // 会在 Done 中返回children map[canceler]struct{} // 子上下文列表,done 被关闭后,会遍历这个 map,关闭所有的子上下文err      error                 // 关闭 chan 产生的异常,在初始化时会被赋值使不为空
}func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()if c.done == nil {c.done = make(chan struct{})}d := c.donec.mu.Unlock()return d
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }
}

当调用 WithCancel 时, 首先会根据 parent 拷贝一个新的 cancelCtx:

func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}

然后会调用 propagateCancel 安排子上下文在父上下文结束时结束,最后除了 cancelCtx 的引用外还会返回一个 func, 该方法里调用了 c.cancel(), 也就是当我们调用 done() 时,调用的其实是 c.cancel()

cancel

cancel 的作用是关闭 当前上下文以及子上下文的cancelCtx.done 管道。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {// 必须要有关闭的原因if err == nil {panic("context: internal error: missing cancel error")}c.mu.Lock()if c.err != nil {c.mu.Unlock()return     // 已经关闭,返回}c.err = err    // 通过 err 标识已经关闭if c.done == nil {c.done = closedchan} else {close(c.done)   // 关闭当前 done}// 由于是 map, 所以关闭顺序是随机的for child := range c.children {child.cancel(false, err)   // 遍历取消所有子上下文}c.children = nil    // 删除子上下文c.mu.Unlock()if removeFromParent {removeChild(c.Context, c)   // 从父上下文删除自己}
}

propagateCancel

该函数的作用是保证父上下文结束时子上下文也结束,一方面,在生成子上下文的过程中,如果父亲已经被取消,那 child 也会被关闭,另一方面,如果在执行过程中父上下文一直开启,那就正常把子上下文加入到父上下文的 children 列表中等执行 cancel再关闭。

func propagateCancel(parent Context, child canceler) {done := parent.Done()// 如果父亲的 Done 方法返回空,说明父上下文永远不会被取消// 这种情况对应 ctx, done := context.WithCancel(context.Background())if done == nil {return }// 如果到这父上下文已经被取消了,就关闭当前上下文select {case <-done:child.cancel(false, parent.Err())returndefault:}// 父亲没有被取消if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()// 父亲已经取消,关闭自己if p.err != nil {child.cancel(false, p.err)} else {// 把 child 加到 parent 的 children 中if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else {// 父上下文是开发者自定义的类型, 开启一个 goroutine 监听父子上下文直到其中一个关闭atomic.AddInt32(&goroutines, +1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}

WithTimeout 和 WithDeadline

type timerCtx struct {cancelCtxtimer *time.Timerdeadline time.Time
}func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}

timerCtx是在 cancelCtx的基础上添加了一个定时器和截止时间实现的。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {// 如果传入的截止时间比父上下文的截止时间晚,也就是说父上下文一定会比子上下文先结束// 这种情况下给子上下文设置截止时间是没有任何意义的,所以会直接创建一个 cancelCtxif cur, ok := parent.Deadline(); ok && cur.Before(d) {return WithCancel(parent)}// 构建新的 timerCtxc := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}// 保证子上下文在父上下文关闭时关闭propagateCancel(parent, c)// 计算当前距离截止时间 d 还有多长时间dur := time.Until(d)// 如果已经过了截止时间,关闭子上下文if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}c.mu.Lock()defer c.mu.Unlock()// c.err == nil 说明当前上下文还没有被关闭if c.err == nil {// AfterFunc 等待 dur 后会开启一个 goroutine 执行 传入的方法,即 c.cancel// 并会返回一个计时器 timer,通过调用 timer 的 Stop 方法可以停止计时取消调用。c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}

timerCtxcancel 方法主要还是调用了 cancelCtx.cancel

func (c *timerCtx) cancel(removeFromParent bool, err error) {// 调用 cancelCtx.cancel,关闭子上下文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()
}

WithTimeout 直接调用了 WithDeadline

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

WithValue

func WithValue(parent Context, key, val interface{}) Context {// key 不能为 nilif key == nil {panic("nil key")}// key 必须是可比较的if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}type valueCtx struct {Contextkey, val interface{}
}

The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys.

key 请尽量使用自定义的 struct{}, 避免使用内置数据类型以避免使用 context 包时的冲突

总结

context 包是 Go 1.7 后加入的一种用于复杂场景下并发控制的模型,最核心的接口是 context.Context, 这个结构体中定义了五个待实现的方法,用来实现发送关闭信号,设置 dateline,传递值等功能。

context 包的核心思想是以 树形 组织 goroutine, 创建新上下文时需要给他指定一个父上下文,由此,根上下文对应根 goroutine, 子上下文对应子 Goroutine, 实现灵活的并发控制。

rootContext 一般通过 Background()TODO() 创建,他们会创建一个空的 emptyCtx, 然后如果想要使用 context 包的具体功能,可以使用 WithCancel()WithDateline()WithValue() 将父上下文包装成具体的上下文对象(cancelCtx, timerCtx, valueCtx),前两个方法会返回两个值 (ctx Context, done func()) 调用 done 可以向 goroutine 发送一个关闭信号, goroutine 中监控 ctx.Done() 便可得到这个信号。

cancelCtxtimerCtx 会保持一个 childrentimerCtx 实际上是继承了 cancelCtx),这是一个 map key 是 canceler , Value 是 struct{} 类型,值并没什么用,在创建 cancelCtxtimerCtx时,会把当前上下文加入到其父亲的 children 中,在父上下文关闭时会遍历 children 关闭所有的子上下文,并将本上下文从其父上下文的 children 中删除,由于 map 遍历的无序性,子上下文关闭的顺序也是随机的。

WithValue() 以及 valueCtx 的实现稍微与前两个有所不同,一方面 valueCtx 没有自己实现 Done(), Deadline() 等方法,所以其功能仅限于传值,另外,在 WithValue() 中并没有调用 propagateCancel(), 所以 valueCtx 并不会被放在父上下文的 children 中,他自己也没有 children, 所以使用 valueCtx 作为父上下文是没有意义的。

如非必要,一般无需使用 WithValue() 的功能传值,他一般用在传递请求对应用户的认证令牌或用于进行分布式追踪的请求 ID中。

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

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

相关文章

iOS开发UI篇—模仿ipad版QQ空间登录界面

一、实现和步骤 1.一般ipad项目在命名的时候可以加一个HD,标明为高清版 2.设置项目的文件结构&#xff0c;分为home和login两个部分 3.登陆界面的设置 &#xff08;1&#xff09;设置第一个控制器和自定义的控制器类&#xff08;登陆&#xff09;关联 &#xff08;2&#xff09…

oracle中scn(系统改变号)

系统scn&#xff1a; select checkpoint_change# from v$database; 文件scn&#xff1a; select name,checkpoint_change# from v$datafile; 结束scn&#xff1a; select name,last_change# from v$datafile; 数据文件头部scn…

三个数相减的平方公式_快收好这份小学数学公式大全!孩子遇到数学难题时肯定用得上...

必背定义、定理公式1.三角形的面积&#xff1d;底高2 公式 S&#xff1d; ah22.正方形的面积&#xff1d;边长边长公式 S&#xff1d; aa3.长方形的面积&#xff1d;长宽公式 S&#xff1d; ab4.平行四边形的面积&#xff1d;底高公式 S&#xff1d; ah5.梯形的面积&#xff1d…

add.attribute向前端传_前端知识-概念篇

1、一次完整的HTTP事务是怎样的一个过程&#xff1f;基本流程&#xff1a;a. 域名解析b. 发起TCP的3次握手c. 建立TCP连接后发起http请求d. 服务器端响应http请求&#xff0c;浏览器得到html代码e. 浏览器解析html代码&#xff0c;并请求html代码中的资源f. 浏览器对页面进行渲…

【数据库】一篇文章搞懂数据库隔离级别那些事(LBCC,MVCC)

MySQL 事务 文章比较长&#xff0c;建议分段阅读 后续如果有改动会在 Junebao.top 之前对事务的了解仅限于知道要么全部执行&#xff0c;要么全部不执行&#xff0c;能背出 ACID 和隔离级别&#xff0c;知其然但不知其所以然&#xff0c;现在觉得非常有必要系统学一下&#xff…

api商品分享源码_SSM框架高并发和商品秒杀项目高并发秒杀API源码免费分享

前言&#xff1a;一个整合SSM框架的高并发和商品秒杀项目,学习目前较流行的Java框架组合实现高并发秒杀API源码获取&#xff1a;关注头条号转发文章之后私信【秒杀】查看源码获取方式&#xff01;项目的来源项目的来源于国内IT公开课平台,质量没的说,很适合学习一些技术的基础,…

Golang 定时任务 github/robfig/cron/v3 使用与源码解析

Cron 源码阅读 robfig/cron/v3 是一个 Golang 的定时任务库&#xff0c;支持 cron 表达式。Cron 的源码真实教科书级别的存在&#xff08;可能是我菜 …&#xff09;,真的把低耦合高内聚体现地淋漓尽致&#xff0c;另外其中涉及的装饰器模式&#xff0c;并发处理等都很值得学习…

修改 cmd 字体为 Consolas

windows 下的 cmd 窗口默认的字体有点难看&#xff0c;长时间使用操作 node.js 有点小疲劳&#xff0c;可以修改注册表替换字体为 Consolas&#xff0c;并且可以全屏 cmd 窗口&#xff0c;代码如下&#xff1a; Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\Conso…

关于 HTTP 的一切(HTTP/1.1,HTTP/2,HTTP/3,HTTPS, CORS, 缓存 ,无状态)

HTTP 为什么会出现 HTTP 协议&#xff0c;从 HTTP1.0 到 HTTP3 经历了什么&#xff1f;HTTPS 又是怎么回事&#xff1f; HTTP 是一种用于获取类似于 HTML 这样的资源的 应用层通信协议&#xff0c; 他是万维网的基础&#xff0c;是一种 CS 架构的协议&#xff0c;通常来说&…

AS 2.0新功能 Instant Run

Instant Run上手作为一个Android开发者&#xff0c;很多的时候我们需要花大量的时间在bulid&#xff0c;运行到真机&#xff08;虚拟机&#xff09;上&#xff0c;对于ios上的Playground羡慕不已&#xff0c;这种情况将在Android Studio 2.0有了很大改善&#xff0c;使用instan…

MySQL InnoDB 是如何存储数据的

InnoDB 是怎么存储数据的 本文是《MySQL 是怎样运行的 —— 从根儿上理解 MySQL》读书总结&#xff0c;强烈推荐这本书&#xff1b; CSDN 不能显示 SVG&#xff0c;可能有图片加载不出来&#xff0c;可以到 我的博客 上看。 数据目录 众所周之&#xff0c;MySQL 的数据是存储在…

WebSocket实战之————GatewayWorker使用笔记例子

参考文档&#xff1a;http://www.workerman.net/gatewaydoc/ 目录结构 ├── Applications // 这里是所有开发者应用项目 │ └── YourApp // 其中一个项目目录&#xff0c;目录名可以自定义 │ ├── Events.php // 开发者只需要关注这个文件 │ ├── st…

[转]关于凸优化的一些简单概念

没有系统学过数学优化&#xff0c;但是机器学习中又常用到这些工具和技巧&#xff0c;机器学习中最常见的优化当属凸优化了&#xff0c;这些可以参考Ng的教学资料&#xff1a;http://cs229.stanford.edu/section/cs229-cvxopt.pdf&#xff0c;从中我们可以大致了解到一些凸优化…

centos7部署两个mysql_一文掌握mysql实用工具--pt-online-schema-change、innotop部署

概述因为OSC和innotop这两个需要的依赖包比较接近&#xff0c;所以这次就写一起了&#xff0c;下面介绍下完整的部署教程&#xff0c;以下基于centos7操作系统。官网文档&#xff1a;http://dev.mysql.com/doc/refman/5.7/en/innodb-create-index-overview.htmlOSC&#xff1a;…

python面试题目

问题一&#xff1a;以下的代码的输出将是什么? 说出你的答案并解释。 1234567891011121314class Parent(object):x 1class Child1(Parent):passclass Child2(Parent):passprint Parent.x, Child1.x, Child2.xChild1.x 2print Parent.x, Child1.x, Child2.xParent.x 3print …

修改页面后获得flag_互动征集丨是时候为2021立flag了

2020马上就要过去了今年的flag各位小伙伴实现了多少&#xff1f;翻出了生灰的flag擦擦说不定2021还能接着用哦2020年就要过去了还记得你在年初立下的那些Flag吗&#xff1f;减肥“明天我就开始减肥&#xff01;”是大部分人在大部分时候都挂在嘴边的一句话疫情宅家不仅没减成还…

为ESXI 添加ISCSI存储设备 Linux服务器系统

为ESXI 添加ISCSI存储设备 Linux系统本文使用的LINUX 6系统上一块硬盘制作的ISCSI存储设备其IP地址为&#xff1a;192.168.26.218:在系统上直接输入&#xff1a;yum -y install scsi-target-utils 命令 安装 iscsi分区设置我们将SDD这块硬盘的SDD1作为iscsi存储设备编辑ISCSI配…

出栈顺序 与 卡特兰数(Catalan)的关系

一&#xff0c;问题描述 给定一个以字符串形式表示的入栈序列&#xff0c;请求出一共有多少种可能的出栈顺序&#xff1f;如何输出所有可能的出栈序列&#xff1f; 比如入栈序列为&#xff1a;1 2 3 &#xff0c;则出栈序列一共有五种&#xff0c;分别如下&#xff1a;1 2 3、…

cad多段线画圆弧方向_CAD箭头怎么画

CAD箭头怎么画问&#xff1a;CAD箭头怎么画&#xff1f;答&#xff1a;想要回答CAD箭头怎么画这个问题&#xff0c;得先从CAD多段线命令说起&#xff0c;画箭只是多段线的一种应用。执行CAD多段线命令的三种方式1.单击菜单栏上的"绘图">>"多段线"。2…

php 获取delete蚕丝_php结合Redis实现100万用户投票项目,并实时查看到投票情况的案例...

场景&#xff1a;某网站需要对其项目做一个投票系统&#xff0c;投票项目上线后一小时之内预计有100万用户进行投票&#xff0c;希望用户投票完就能看到实时的投票情况这个场景可以使用redismysql冷热数据交换来解决。何为冷热数据交换&#xff1f;冷数据&#xff1a;之前使用的…