go 的 Context

  • 一直对 go 的 Context 一知半解,不了解其用途,因此在这里着重了解一下 go 语言的 Context
  • 飞雪无情的一个博文对 go 的 Context 讲的比较易懂一些,所以就先从这篇博文开始吧

常用的并发控制

  • 飞雪无情博客中提到,常用的并发控制是通过 sync 包中的 WaitGroup 来实现的
// 使用 sync 包中的 WaitGroup 实现协程的并发控制
// 其使用场景是:多个协程分别完成一整件事的一部分的工作,等待前部协程都完成之后,才算是完成了一整件事
func contextPart1() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
time.Sleep(1 * time.Second)
log.Println("协程 1号")
wg.Done()
}()
go func() {
time.Sleep(2 * time.Second)
log.Println("channel 2 is complete")
wg.Done()
}()
wg.Wait()
log.Println("所有协程都执行完成~")
}

channel + select

  • 以上这种场景主要是针对,可以自行结束的协程的并发控制。如果遇到的场景是协程并不会自动结束,该如何处理呢?
  • 有一种方式,就是在任务协程中监听一个 channel,这个 channel 中一旦有数据(控制信号)就意味着对任务协程状态的控制
// 通过通知协程结束的方式控制协程
func contextPart2() {
stopCh := make(chan bool)
go func() {
for {
select {
case <-stopCh:
fmt.Println("协程即将结束了")
return
default:
fmt.Println("默认情况,继续执行...")
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("通知协程要结束了")
stopCh <- true
time.Sleep(5 * time.Second)
}
  • 上面代码中,一开始创建了一个 stopCh 的通道,表示任务协程终止信号。任务协程中,通过 select 语句监听 stopCh 是否有数据,如果有数据,则实现协程任务结束操作,也就是 return。
  • 以下是飞雪博客中对这总 channel + select 的方式的评价:

这种chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他更多的goroutine怎么办呢?如果一层层的无穷尽的goroutine呢?这就非常复杂了,即使我们定义很多chan也很难解决这个问题,因为goroutine的关系链就导致了这种场景非常复杂。

使用 Context

  • 因为会有 goroutine 中开启 goroutine 的情况,为了能够更优雅的管理 goroutine,go 中引入了 Context。将上面的例子,使用 Context 的方式重写:
// 使用 Context 方式管理 goroutine
func contextPart3() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
log.Println("任务完成,结束...")
return
default:
log.Println("goroutine 继续执行...")
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
log.Println("通知任务协程,要结束了")
cancel()
time.Sleep(5 * time.Second)
}
  • 看起来,跟 channel+select 的方式差不多嘛。实际的情况是使用 Context 方式可以更方便地控制、跟踪 goroutine。

context.Background() 返回一个空的 Context,这个空的 Context 一般用于整个 Context 树的根节点。然后我们使用 context.WithCancel(parent) 函数,创建一个可取消的子 Context,然后当作参数传给 goroutine 使用,这样就可以使用这个子 Context 跟踪这个 goroutine。

  • 在 goroutine 中,使用 <-ctx.Done() 来判断是否要结束。如果有值,则对应的分支直接 return。如果没有值,则继续处理对应的任务。
  • 在 channel+select 的方式中,我们可以向对应的 channel 发送数据表示停止信号,那么 Context 的方式如何发送信号呢?
  • 就是位于上方代码中的 cancel() 调用。它是 context.WithCancel 返回的 CancelFunc 类型

Context 控制多个 goroutine

  • 因为实际的场景中是非常的复杂而多样化的,一定存在着多个 goroutine,针对多个 goroutine,Context 是如何处理的呢?
// 使用 Context 控制多个 goroutine
func contextPart4() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "task 1")
go watch(ctx, "task 2")
go watch(ctx, "task 3") time.Sleep(10 * time.Second)
log.Println("可以通知任务结束")
cancel()
time.Sleep(5 * time.Second)
} func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
log.Println(name + " 任务即将要退出了...")
return
default:
log.Println(name + " goroutine 继续处理任务中...")
time.Sleep(2 * time.Second)
}
}
}
  • 通过实际的运行,可以看到只需要调用一次 cancel() 就能控制多个 goroutine

Context 接口

  • 通过查看官方代码,可以看到 Context 的接口:
// 一个 Context 可以跨 API 携带截止时间、取消信号以及其他值
//
// Context 的方法可以由多个 goroutine 同时调用
type Context interface {
// Deadline 返回代表上下文任务应该被取消的截止时间。当没有设置截止时间时,Deadline 将返回 ok==false。对 Deadline 的连续调用将返回相同的结果。
Deadline() (deadline time.Time, ok bool) // Done 返回一个 channel,该 channel 在该上下文的任务被取消时应该被关闭。如果无法取消这个上下文,Done 可能返回 nil。对 Done 的连续调用将返回相同的值
Done() <-chan struct{} // 如果 Done 没有关闭,Err 将返回 nil。
// 如果 Done 已关闭,Err 返回一个非 nil 错误,原因是:如果上下文已被取消,则返回 cancel;如果上下文的截止时间已过,则返回 deadline。
// 在 Err 返回非 nil 错误之后。对 Err 的连续调用将返回相同的错误。
Err() error // Value 返回针对 key 上下文的关联的值,如果没有与 key 关联的值,则返回 nil。使用相同的 key 连续调用 Value 将返回一样的结果。
Value(key interface{}) interface{}
}
  • 其中有 4 个方法,相关的注释已经翻译为中文,可以了解一下各个方法的作用。
  • 在 context 包中,已经实现了 2 种 Context,分别是 Background 和 TODO。从包的官方文档中可以看到:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
) // Background 返回一个非 nil 的空 Context。它不会被取消,没有值,也没有截止时间。它通常用于 main 函数、初始化和测试
// 并作为传入请求的顶层的 Context
func Background() Context {
return background
} // TODO 返回一个非 nil 的空 Context。当不清楚要使用哪个 Context 或者还不能用 Context 时,代码应该使用 context.TODO
// (因为周边函数还没有扩展到接收一个 Context 参数)
func TODO() Context {
return todo
}
  • 从定义中可以看到没有什么特别复杂的东西,但是我们需要注意一下 emptyCtx。包中对它的定义如下:
type emptyCtx int

emptyCtx 不会被取消,它没有值,也没有截止时间。它不是 struct{},因为这种类型的变量必须有不同的地址

  • 这就是为什么 background 和 todo 明明类型一样,却还 new 两次。下面看一下 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
} func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
  • 貌似大部分方法都是直接返回一个 nil
  • 在上面的 contextPart3、contextPart4 中,我们都是在 main 中创建 goroutine,我们讲过,实际的场景中,会有在子 goroutine 中创建 goroutine 的情况,这种时候,改如何创建呢?难道是还是使用 ctx, cancel := context.WithCancel(context.Background()) 创建吗?
  • 在回答这个问题前,我们先了解一下 With* 系列的方法:
// WithCancel 返回父级的 Done 通道的副本。无论一开始发生什么,当调用其返回的 cancel 函数或当关闭父级 Context 的 Done 通道时,都将关闭返回的上下文的 Done 通道。
// 取消此 Context 将释放与其关联的资源,因此,代码应该在此上下文中运行的操作完成后立即调用 cancel。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc); // WithCancel 将返回一个父级 context 的副本,该副本带有截止时间调整为不迟于 d。如果父级上下文的截止时间比 d 要早,
// 则 WithDeadline(parent, d) 在语义上等同于父级 context。当截止时间过期时,或当调用返回的 cancel 时,
// 或当父级 context 的 Done 通道被关闭时,返回的 context 的 Done 通道将关闭,以最先发生的情况为准。 // 取消此上下文将释放与其资源关联的资源,因此,在此 context 中运行的操作完成后应该立即调用 cancel。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc); // WithTimeout 将返回 `WithDeadline(parent, time.Now().Add(timeout))` 参数
// 取消这个 context将释放与它相关联的资源,所以代码中应该在这个上下文中运行的操作完成后立即调用cancel:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc); // WithValue 返回父级的一个副本,其中与 key 关联的值是 val
// 只对传输进程和 API 的请求域数据使用 context Value,而不是将可选参数传递给函数的场景 // 提供的 key 必须是可比较的,并且不应该是 string 或者其他内置类型,这样可以避免使用 context 在包之间发生冲突。
// 使用 WithValue 的用户应该为 key 定义自己的类型。为了避免在分配时分配给 interface{},context key 通常有具体的类型 struct{}
// 或者,导出的上下文关 key 变量的静态类型应该是一个指针或者 interface。
func WithValue(parent Context, key, val interface{}) Context;
  • 这 4 个函数,参数中都有 parent Context,也就是父级 Context,要达到“在子 goroutine 中创建 goroutine”,其实就是基于这个“父级 Context”来创建。可以将其称之为“衍生”
  • 从抽象的角度看,“子 goroutine 中创建 goroutine ”达到一定程度可以将总体看成一颗 Context 树

树的每个节点都可以有任意多个子节点,节点层级可以有任意多个

  • 我们可以观察到,With* 系列函数中的前三个都会返回 CancelFunc 类型:
// CancelFunc 的主要作用就是放弃其任务的操作。CancelFunc 不会等待其任务停止
// 在第一次调用 CancelFunc 后,后续的调用将什么都不会做。也就是只会生效一次。
type CancelFunc func()

WithValue

  • 由于在“子 goroutine 中创建 goroutine ”的过程中,我们可能需要在比较深的 goroutine 中需要使用外层就产生的一些数据,此时我们可以使用 WithValue
  • WithValue 可以帮我们传递一些必须的元数据,这些数据会附加在 Context 中
func contextPart5() {
var key string = "key1"
ctx, cancel := context.WithCancel(context.Background())
// 附加的数据
vCtx := context.WithValue(ctx, key, "这里是元数据-任务1")
go watch2(vCtx, "task1")
time.Sleep(10 * time.Second)
log.Println("可以通知任务停止了...")
cancel()
time.Sleep(5 * time.Second)
} func watch2(ctx context.Context, name string) {
var key string = "key1"
for {
select {
case <-ctx.Done():
log.Println("获取到元数据:" + ctx.Value(key).(string))
log.Println(name + " 任务即将要退出了...")
return
default:
log.Println(name + " goroutine 继续处理任务中...")
time.Sleep(2 * time.Second)
}
}
}
  • 通过运行并观察,输出如下:
2019/07/01 17:22:54 task1 goroutine 继续处理任务中...
2019/07/01 17:22:56 task1 goroutine 继续处理任务中...
2019/07/01 17:22:58 task1 goroutine 继续处理任务中...
2019/07/01 17:23:00 task1 goroutine 继续处理任务中...
2019/07/01 17:23:02 task1 goroutine 继续处理任务中...
2019/07/01 17:23:04 可以通知任务停止了...
2019/07/01 17:23:04 获取到元数据:这里是元数据-任务1
2019/07/01 17:23:04 task1 任务即将要退出了...
  • 内存的 goroutine 可以正确通过 context 获取到对应的元数据。值的注意的是,这里的值必须是线程安全的。

Context 使用原则

  • 以下是飞雪无情博客中提到的一些使用原则,为了能够更好、更准确的使用 Context,我们最好遵循:
  • 1.不要把Context放在结构体中,要以参数的方式传递
  • 2.以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  • 3.给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  • 4.Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  • 5.Context是线程安全的,可以放心的在多个goroutine中传递
  • 文中提到的代码可以在 GitHub 中找到。

参考资料

了解 go 的 Context的更多相关文章

  1. Javascript 的执行环境(execution context)和作用域(scope)及垃圾回收

    执行环境有全局执行环境和函数执行环境之分,每次进入一个新执行环境,都会创建一个搜索变量和函数的作用域链.函数的局部环境不仅有权访问函数作用于中的变量,而且可以访问其外部环境,直到全局环境.全局执行环境 ...

  2. spring源码分析之<context:property-placeholder/>和<property-override/>

    在一个spring xml配置文件中,NamespaceHandler是DefaultBeanDefinitionDocumentReader用来处理自定义命名空间的基础接口.其层次结构如下: < ...

  3. spring源码分析之context

    重点类: 1.ApplicationContext是核心接口,它为一个应用提供了环境配置.当应用在运行时ApplicationContext是只读的,但你可以在该接口的实现中来支持reload功能. ...

  4. CSS——关于z-index及层叠上下文(stacking context)

    以下内容根据CSS规范翻译. z-index 'z-index'Value: auto | <integer> | inheritInitial: autoApplies to: posi ...

  5. Tomcat启动报错org.springframework.web.context.ContextLoaderListener类配置错误——SHH框架

    SHH框架工程,Tomcat启动报错org.springframework.web.context.ContextLoaderListener类配置错误 1.查看配置文件web.xml中是否配置.or ...

  6. mono for android Listview 里面按钮 view Button click 注册方法 并且传值给其他Activity 主要是context

    需求:为Listview的Item里面的按钮Button添加一个事件,单击按钮时通过事件传值并跳转到新的页面. 环境:mono 效果: 布局代码 主布局 <?xml version=" ...

  7. Javascript的“上下文”(context)

    一:JavaScript中的“上下文“指的是什么 百科中这样定义: 上下文是从英文context翻译过来,指的是一种环境. 在软件工程中,上下文是一种属性的有序序列,它们为驻留在环境内的对象定义环境. ...

  8. spring源码分析之<context:component-scan/>vs<annotation-config/>

    1.<context:annotation-config/> xsd中说明: <xsd:element name="annotation-config"> ...

  9. 【Android】 context.getSystemService()浅析

    同事在进行code review的时候问到我context中的getSystemService方法在哪实现的,他看到了一个ClipBoardManager来进行剪切板存储数据的工具方法中用到了cont ...

  10. context:component-scan" 的前缀 "context" 未绑定。

    SpElUtilTest.testSpELLiteralExpressiontestSpELLiteralExpression(cn.zr.spring.spel.SpElUtilTest)org.s ...

随机推荐

  1. 测试用例与PUCCH

  2. gitlab 更换服务器后访问 Integrations 出现 500 错误

    异常问题解决方案:问题:gitlab 更换服务器后访问 Integrations 出现 500 错误解决方案:从原服务器上将 /etc/gitlab/gitlab-secrets.json 复制过来覆 ...

  3. npm 模块开发调试技巧之最优方案npm link

    在我们平时写项目中,当我们需要新开发或修改的 npm 模块时,如何在本地项目中调试呢? 本地项目路径:G:\npm\project 开发的模块路径:G:\npm\model 方法一: 在cmd命令窗口 ...

  4. Eclipse项目工程导入到IDEA继续开发-超详细

    现在Java开发的主流工具是IDEA,不是说Eclipse,各有各的特色.不过我现在深深的爱上了idea这个工具. 但是之前很多项目都是用eclipse开发的,现在就转入到idea中进行继续开发. 1 ...

  5. jave的安装

    1.此电脑-属性-高级系统设置-环境变量2.点下面那个 新建-  JAVA_HOME3. 双击PATH变量,新建一个参数 4.新建CLASSPATH环境变量

  6. SpringBoot框架---配置

    1.更改tomcat的端口号: application.properties 配置文件中, server.port=9090 2.设置上下文路径: server.servlet.context-pat ...

  7. WMITools修复wmi劫持--hao643.com/jtsh123劫持(修改快捷方式跳转至hao123.com)

    >症状:所有浏览器快捷方式,都被加上尾巴,例如IE的:"C:\Program Files\Internet Explorer\iexplore.exe" http://hao ...

  8. hdu 5917

    题意:给你一个无向图,问图中有多少个符合条件的集合?条件为这个集合里面存在一个子集(大小>=3)为团或者都是孤立点.答案mod1e9+7: 根据 Ramsey定理,大于等于6个的集合,肯定存在一 ...

  9. LED Craft Light - How To Solve: Home Decoration Lighting

    Home décor usually comes with a certain period of theme or a specific style of furniture, which will ...

  10. laydate 限制结束日期不能大于起始日期

    时间选择器在选择的时候,同时配置了另一个时间选择器内的参数 <div class="form-group"> <label for="exampleIn ...