0
点赞
收藏
分享

微信扫一扫

Go 语言 怎么使用 Content 控制 goroutine 协程关闭


1.Context的用途

简单来说,context就是在golang中防止内存泄露来进行goroutine并发控制的。

Golang内置了context包,context.Context是一个结构体,用来设置截止时间、同步信号、传递请求相关知识等

他的底层是通过 channel 去传递管理信号。例如,父 goroutine 可以传递一个close信号来关闭此channel,子 goroutine 通过监听这个channel,那么子goroutine也会退出。

但是在go中,goroutine之间都是平等调度的,并没有父子goroutine的概念,我们只是人为的给有当前goroutine所创建出的goroutine认为是当前goroutine的子goroutine。因此也就不存在当父goroutine退出之后子goroutine会自动退出,也就是会导致所创建的内存不能够及时的释放,内存发生泄漏。因此context就由此诞生来解决上述问题。

2.Context怎么工作的:

(1)Context基本数据结构

在聊其怎么工作的之前先要明白Context接口,所有的Context对象都实现了Context接口,Context接口实现了几种基本的功能方法,有Deadline()实现了超时控制、Value(key interface{})interface{}来访问goroutine之间的传递的值等。

Context对象在被第一个goroutine所创建的时候,我们就认为当前这个goroutine就是root节点(也称为根节点),root节点负责创建一个实现Context接口的具体对象,但是这个具体的对象是一个empty Context结构,也就说这个emptyContext是不具备任何的功能的,他就是一个Context实例,作为Context对象树的根节点(root节点),有了这个root节点之后,后面所有Context的创建都是以上一个Context对象作为参数,在当前root节点的基础上,不断的封装功能,最终形成了一个树状的结构。

Context提供了两种实现empty Context的全局变量和两个结构体实例

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

这两个结构体通常都会作为root实例化对象节点传入首层的goroutine当中,来构造Context的取消数的根节点对象。

(2)Context的提供的功能:

Context提供了下面四种方法:
​​​WithCancel()​​​​WithDeadline()​​​​WithTimeout()​​​​WithValue()​​ 这四种方法都必须传入他们的父Context作为参数来实现。并且在goroutine的调用链过程中,Conterxt会被逐层的包装起来,并且一层一层的传递下去,每一层都可以对Context封装自己所特有的功能,最终实现了一个调用树。

(3)Context是如何实现退出通知机制的:

Context的WithCancel是来实现退出通知的

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
...
// 实例化一个子 context
c := newCancelCtx(parent)
// 父和子绑定关系
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

上文说到Context在内存中实际上维护了一个树状的goroutine调用链,但是其主要是依靠一个函数来实现的,而这个调用链也是由这个函数来维护的,:
​​​propagateCancel(parent Context,child canceler)​

接下来我们来看看这个函数

作者:hyad
链接:https://www.zhihu.com/question/504542458/answer/2563474714
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

func propagateCancel(parent Context, child canceler) {
...
if p, ok := parentCancelCtx(parent); ok {
// 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号;
p.mu.Lock()
if p.err != nil {
// 如果已经被取消,child 会立刻被取消;
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
// 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel
// 在 parent.Done() 关闭时调用 child.cancel 取消子上下文;
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}

从源码中可以看到,在调用链的过程中,会调用一个goroutine同时监听parent.Done()和child.Done(),当有parent.Done()关闭的时候就会调用child.cancel的方法来取消上下文,同时会在构造树上将该child节点删掉,删掉此child节点之后,还会轮训的将此child节点的child节点全部都调用cancel函数使其退出,最终实现了此parent节点的全部孩子节点都退出。

、​


举报

相关推荐

0 条评论