golang优雅的使用context
文章目录
- 优雅的关闭goroutine
- sync.WaitGroup 实现
- channel + select 实现
- context 实现
- 优雅的关闭多个goroutine嵌套
- net/http包中的context
- 关于Context传递数据
- 小结
优雅的关闭goroutine
在很多场景下,在执行一个任务时,我们会将这个任务拆分成几个子任务,然后开启几个不同的goroutine去执行。当因为某些原因这个任务需要终止时,我们需要将这些goroutine也全都终止掉。
比如Go http包的Server中,每一个请求都有对应的goroutine去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。
下面我们举一个简单的例子,然后分别使用不同的方法去关闭例子中的goroutine。
sync.WaitGroup 实现
package mainimport ("fmt""strconv""sync""time"
)var wg sync.WaitGroupfunc run(task string) {fmt.Println(task, "start...")time.Sleep(time.Second * 2)// 每个groutine运行完毕后就释放WaitGroup的计时器wg.Done()
}func main() {wg.Add(2)for i := 1; i < 3; i++ {taskName := "task" + strconv.Itoa(i)go run(taskName)}wg.Wait()fmt.Println("所有任务结束")
}// task2 start...
// task1 start...
// 所有任务结束
上面例子中,一个任务结束了必须等待另外一个任务也结束了才算全部结束了,先完成的必须等待其他未完成的,所有的goroutine都要全部完成才OK。
这种方式的优点:使用等待组的并发控制模型,尤其适用于好多个goroutine协同做一件事情的时候,因为每个goroutine做的都是这件事情的一部分,只有全部的goroutine都完成,这件事情才算完成。
这种方式的缺陷:在实际生产中,需要我们主动的通知某一个 goroutine 结束。
channel + select 实现
package mainimport ("fmt""time"
)func main() {stop := make(chan bool)go func() {for {select {case <- stop:fmt.Println("任务1 结束了")returndefault:fmt.Println("任务1 运行中")time.Sleep(time.Second)}}}()// 运行5秒后停止time.Sleep(time.Second * 5)stop <- true// 停止检测goroutine是否已经结束time.Sleep(time.Second * 3)
}// 任务1 运行中
// 任务1 运行中
// 任务1 运行中
// 任务1 运行中
// 任务1 运行中
// 任务1 结束了
使用channel + select的优点:比较优雅。
使用channel + select的缺点:如果有多个goroutine需要关闭怎么办?可以使用全局bool类型变量的方法,但是在为全局变量赋值的时候需要用到锁来保证协程安全,这样势必会对便利性和性能造成影响。更有甚者,如果每个goroutine中又嵌套了goroutine呢?
context 实现
将以上的代码使用context重写:
package mainimport ("context""fmt""time"
)func main() {ctx, cancel := context.WithCancel(context.Background())// 开启goroutine,传入ctxgo func(ctx context.Context) {for {select {case <- ctx.Done():fmt.Println("任务1 结束了")returndefault:fmt.Println("任务1 运行中")time.Sleep(time.Second)}}}(ctx)// 运行五秒以后停止time.Sleep(time.Second * 5)cancel()// 停止检测goroutine是否已经结束time.Sleep(time.Second * 3)
}// 任务1 运行中
// 任务1 运行中
// 任务1 运行中
// 任务1 运行中
// 任务1 运行中
// 任务1 结束了
使用context重写比较简单,当然上述只是启动一个goroutine的情况,如果有多个goroutine呢?
package mainimport ("context""fmt""time"
)// 使用context控制多个goroutine
func watch(ctx context.Context, name string) {for {select {case <- ctx.Done():fmt.Println(name, "退出, 停止了")returndefault:fmt.Println(name, "运行中")time.Sleep(time.Second)}}
}func main() {ctx, cancel := context.WithCancel(context.Background())go watch(ctx, "任务1")go watch(ctx, "任务2")go watch(ctx, "任务3")time.Sleep(time.Second * 3)// 通知任务停止cancel()time.Sleep(time.Second * 2)fmt.Println("确定任务全部停止")
}//任务3 运行中
//任务1 运行中
//任务2 运行中
//任务2 运行中
//任务3 运行中
//任务1 运行中
//任务1 运行中
//任务2 运行中
//任务3 运行中
//任务2 退出, 停止了
//任务1 退出, 停止了
//任务3 退出, 停止了
//确定任务全部停止
上述Context就像一个控制器一样,按下开关后,所有基于这个 Context 或者衍生的子 Context 都会收到通知,这时就可以进行清理操作了,最终释放 goroutine,这就优雅的解决了 goroutine 启动后不可控的问题。
优雅的关闭多个goroutine嵌套
package mainimport ("context""fmt""time"
)// 定义一个包含context的新类型
type otherContext struct {context.Context
}func work(ctx context.Context, name string) {for {select {case <- ctx.Done():fmt.Println(name, " get msg to cancel")returndefault:fmt.Println(name, " is running")time.Sleep(time.Second)}}
}func workWithValue(ctx context.Context, name string) {for {select {case <- ctx.Done():fmt.Println(name, " get msg to cancel")returndefault:value := ctx.Value("key").(string)fmt.Println(name, " is running value = ", value)time.Sleep(time.Second)}}
}func main() {// 使用context.Background()构建一个WithCancel类型的上下文ctxa, cancel := context.WithCancel(context.Background())// work模拟运行并检测前端的退出通知go work(ctxa, "work1")// 使用WithDeadline包装前面的上下文对象ctxatm := time.Now().Add(3 * time.Second)ctxb, _ := context.WithDeadline(ctxa, tm)go work(ctxb, "work2")// 使用WithValue包装前面的上下文对象ctxboc := otherContext{ctxb}ctxc := context.WithValue(oc, "key", "andes, pass from main")go workWithValue(ctxc, "work3")// 故意 "sleep" 10秒, 让work2、work3超时退出time.Sleep(10 * time.Second)// 显示调用work1的cancel方法通知其退出cancel()// 等待work1打印退出信息time.Sleep(5 *time.Second)fmt.Println("main stop")
}//work3 is running value = andes, pass from main
//work1 is running
//work2 is running
//work2 is running
//work3 is running value = andes, pass from main
//work1 is running
//work1 is running
//work3 is running value = andes, pass from main
//work2 is running
//work3 get msg to cancel
//work2 get msg to cancel
//work1 is running
//work1 is running
//work1 is running
//work1 is running
//work1 is running
//work1 is running
//work1 is running
//work1 get msg to cancel
//main stop
在使用Context的过程中,程序在底层实际上维护了两条关系链。
children key构成从根到叶子Context实例的引用关系,在调用With函数时,会调用propagateCancel(parent Context, child canceler) 函数进行维护,程序有一层这样的树状结构:
ctxa.children --> ctxb ctxb.children --> ctxc
这棵树提供一种从根节点开始遍历树的方法,context包的取消广播通知就是基于这棵树实现的,取消通知沿着这条链从根节点向下层节点逐层广播。当然也可以在任意一个子树上调用取消通知,一样会扩散到整棵树。
在构造 context 的对象中不断地包裹 context 实例形成一个引用关系链,这个关系链的方向是相反的,是自底向上的。
ctxc.Context --> oc ctxc.Context.Context --> ctxb ctxc.Context.Context.cancelCtx --> ctxa ctxc.Context.Context.Context.cancelCtx.Context -> new(emptyCtx) // context.Background()
这个关系链主要用于切断当前Context实例和上层的Context实例之间的关系。,比如 ctxb调用了退出通知或定时器到期了, ctxb 后续就没有必要在通知广播树上继续存在,它需要找到自己的 parent ,然后执行 delete(parent.children,ctxb), 把自己从广播树上清理掉。
net/http包中的context
net/http包源码在实现http server时就用到了context, 下面简单分析一下:
1.首先Server在开启服务时会创建一个valueCtx,存储了server的相关信息,之后每建立一条连接就会开启一个协程,并携带此valueCtx。
func (srv *Server) Serve(l net.Listener) error {...var tempDelay time.Duration // how long to sleep on accept failurebaseCtx := context.Background() // base is always background, per Issue 16220ctx := context.WithValue(baseCtx, ServerContextKey, srv)for {rw, e := l.Accept()...tempDelay = 0c := srv.newConn(rw)c.setState(c.rwc, StateNew) // before Serve can returngo c.serve(ctx)}
}
2.建立连接之后会基于传入的context创建一个valueCtx用于存储本地地址信息,之后在此基础上又创建了一个cancelCtx,然后开始从当前连接中读取网络请求,每当读取到一个请求则会将该cancelCtx传入,用以传递取消信号。一旦连接断开,即可发送取消信号,取消所有进行中的网络请求。
func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())...ctx, cancelCtx := context.WithCancel(ctx)c.cancelCtx = cancelCtxdefer cancelCtx()...for {w, err := c.readRequest(ctx)...serverHandler{c.server}.ServeHTTP(w, w.req)...}
}
3.读取到请求之后,会再次基于传入的context创建新的cancelCtx,并设置到当前请求对象req上,同时生成的response对象中cancelCtx保存了当前context取消方法。
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {...req, err := readRequest(c.bufr, keepHostHeader)...ctx, cancelCtx := context.WithCancel(ctx)req.ctx = ctx...w = &response{conn: c,cancelCtx: cancelCtx,req: req,reqBody: req.Body,handlerHeader: make(Header),contentLength: -1,closeNotifyCh: make(chan bool, 1),// We populate these ahead of time so we're not// reading from req.Header after their Handler starts// and maybe mutates it (Issue 14940)wants10KeepAlive: req.wantsHttp10KeepAlive(),wantsClose: req.wantsClose(),}...return w, nil
}
这样处理的目的主要有以下几点:
- 一旦请求超时,即可中断当前请求;
- 在处理构建response过程中如果发生错误,可直接调用response对象的cancelCtx方法结束当前请求;
- 在处理构建response完成之后,调用response对象的cancelCtx方法结束当前请求。
在整个server处理流程中,使用了一条context链贯穿Server、Connection、Request,不仅将上游的信息共享给下游任务,同时实现了上游可发送取消信号取消所有下游任务,而下游任务自行取消不会影响上游任务。
关于Context传递数据
首先要清楚使用 context 包主要是解决 goroutine 的通知退出,传递数据是其一个额外功能。
可以使用它传递一些元信息 ,总之使用 context 传递的信息不能影响正常的业务流程,程序不要期待在 context 中传递某些必需的参数等,没有这些参数,程序也应该能正常工作。
context应该传递什么数据
1.日志信息。
2.调试信息。
3.不影响业务主逻辑的可选数据。
小结
1.context主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。
2.context 包提供的核心的功能是多 goroutine 之间的退出通知机制,传递数据只是个辅助功能,应谨慎使用 context 传递数据。
参考资料:
《Go语言核心编程》
https://www.cnblogs.com/vinsent/p/11455531.html
https://zhuanlan.zhihu.com/p/110085652
golang优雅的使用context相关推荐
- go get报错unrecognized import path “golang.org/x/net/context”…
今天安装gin框架,首先下载gin,命令如下: go get github.com/mattn/go-sqlite3 结果报错: package golang.org/x/net/context: u ...
- go get报错:unrecognized import path “golang.org/x/net/context”…
今天安装gin框架,首先下载gin,命令如下: go get github.com/mattn/go-sqlite3 结果报错: package golang.org/x/net/context: u ...
- Golang优雅之道
借助一些设计模式.流式编程.函数编程的方法可以让我们的Golang代码更清晰优雅,本文中描述了在错误处理.可选配置.并发控制等方面的优化手段. 链式错误处理 很多人不喜欢Go的错误处理,需要写大量if ...
- Golang中WaitGroup、Context、goroutine定时器及超时学习笔记
原文连接:http://targetliu.com/2017/5/2... 好久没有发过文章了 - -||,今天发一篇 golang 中 goroutine 相关的学习笔记吧,以示例为主. WaitG ...
- Golang 并发编程之Context
Context 是 Golang 中非常有趣的设计,它与 Go 语言中的并发编程有着比较密切的关系,在其他语言中我们很难见到类似 Context 的东西,它不仅能够用来设置截止日期.同步『信号』还能用 ...
- Golang 如何正确使用 Context
视频信息 How to correctly use package context by Jack Lindamood at Golang UK Conf. 2017 视频: https://www. ...
- Golang | 优雅地定义枚举类型
不失优雅地定义枚举类型 枚举实际上是一种派生地数据类型,我们一般用来定义若干常量的集合.我们最常举的例子就是一周七天这种,它是最典型的使用枚举来定义的.枚举是一种特殊使用的常量,Go语言中定义枚举需要 ...
- golang:context介绍
我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战 1 前言 最近实现系统的分布式日志与事务管理时,在寻求所谓的全局唯一Goroutine ID无果之后,决定还是简单利用Conte ...
- Golang底层原理剖析之上下文Context
Context 前言 Context 前言 如何优雅地使用context点击浅谈Golang上下文Context Context 在Go语言并发编程中,用一个goroutine来处理一个任务 ,而它又 ...
最新文章
- 最新最全国内外架构开源技术干货资料
- POJ 1860 Currency Exchange (Bellman-Ford)
- 磁盘格式化与快速格式化有什么区别?
- ASP.NET MVC4 微信公众号开发之网页授权(一):搭建基础环境
- KubeCon直击 | 华为云以技术布道“云边端芯”
- 【BZOJ3997】组合数学,总之是DP就对了
- matlab 判断整除函数_判断素数函数
- Facebook广告兴趣定位终极指南经验分享
- leetcode-40-组合总和 II
- lightslider-支持移动触摸的轻量级jQuery幻灯片插件
- Linux常用工具包安装
- [SharePoint教程系列] 0.SharePoint 2016介绍
- 进击系列2.0:进击的骑士-----用funcode与C语言实现射击游戏制作
- vue实现预览pdf组件(vue-pdf插件使用)
- 《谈谈方法》这本小书篇幅很短,然而想说的却很多
- 创建OMF(Oracle Managed Files,Oracle管理的文件)
- 2012网站服务器目录磁盘满了,服务器磁盘异常爆满的原因及解决方法
- sql 数据与程序的物理独立性和逻辑独立性
- 那些曾让你哭过的事,总有一天会笑着说出来
- 当前超级计算机的应用方兴未艾,(全国通用版)18版高考语文大一轮复习第2周基础组合练4...