文章目录

  • 优雅的关闭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的过程中,程序在底层实际上维护了两条关系链。

  1. children key构成从根到叶子Context实例的引用关系,在调用With函数时,会调用propagateCancel(parent Context, child canceler) 函数进行维护,程序有一层这样的树状结构:

    ctxa.children --> ctxb
    ctxb.children --> ctxc
    

    这棵树提供一种从根节点开始遍历树的方法,context包的取消广播通知就是基于这棵树实现的,取消通知沿着这条链从根节点向下层节点逐层广播。当然也可以在任意一个子树上调用取消通知,一样会扩散到整棵树。

  2. 在构造 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相关推荐

  1. 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 ...

  2. 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 ...

  3. Golang优雅之道

    借助一些设计模式.流式编程.函数编程的方法可以让我们的Golang代码更清晰优雅,本文中描述了在错误处理.可选配置.并发控制等方面的优化手段. 链式错误处理 很多人不喜欢Go的错误处理,需要写大量if ...

  4. Golang中WaitGroup、Context、goroutine定时器及超时学习笔记

    原文连接:http://targetliu.com/2017/5/2... 好久没有发过文章了 - -||,今天发一篇 golang 中 goroutine 相关的学习笔记吧,以示例为主. WaitG ...

  5. Golang 并发编程之Context

    Context 是 Golang 中非常有趣的设计,它与 Go 语言中的并发编程有着比较密切的关系,在其他语言中我们很难见到类似 Context 的东西,它不仅能够用来设置截止日期.同步『信号』还能用 ...

  6. Golang 如何正确使用 Context

    视频信息 How to correctly use package context by Jack Lindamood at Golang UK Conf. 2017 视频: https://www. ...

  7. Golang | 优雅地定义枚举类型

    不失优雅地定义枚举类型 枚举实际上是一种派生地数据类型,我们一般用来定义若干常量的集合.我们最常举的例子就是一周七天这种,它是最典型的使用枚举来定义的.枚举是一种特殊使用的常量,Go语言中定义枚举需要 ...

  8. golang:context介绍

    我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战 1 前言 最近实现系统的分布式日志与事务管理时,在寻求所谓的全局唯一Goroutine ID无果之后,决定还是简单利用Conte ...

  9. Golang底层原理剖析之上下文Context

    Context 前言 Context 前言 如何优雅地使用context点击浅谈Golang上下文Context Context 在Go语言并发编程中,用一个goroutine来处理一个任务 ,而它又 ...

最新文章

  1. 最新最全国内外架构开源技术干货资料
  2. POJ 1860 Currency Exchange (Bellman-Ford)
  3. 磁盘格式化与快速格式化有什么区别?
  4. ASP.NET MVC4 微信公众号开发之网页授权(一):搭建基础环境
  5. KubeCon直击 | 华为云以技术布道“云边端芯”
  6. 【BZOJ3997】组合数学,总之是DP就对了
  7. matlab 判断整除函数_判断素数函数
  8. Facebook广告兴趣定位终极指南经验分享
  9. leetcode-40-组合总和 II
  10. lightslider-支持移动触摸的轻量级jQuery幻灯片插件
  11. Linux常用工具包安装
  12. [SharePoint教程系列] 0.SharePoint 2016介绍
  13. 进击系列2.0:进击的骑士-----用funcode与C语言实现射击游戏制作
  14. vue实现预览pdf组件(vue-pdf插件使用)
  15. 《谈谈方法》这本小书篇幅很短,然而想说的却很多
  16. 创建OMF(Oracle Managed Files,Oracle管理的文件)
  17. 2012网站服务器目录磁盘满了,服务器磁盘异常爆满的原因及解决方法
  18. sql 数据与程序的物理独立性和逻辑独立性
  19. 那些曾让你哭过的事,总有一天会笑着说出来
  20. 当前超级计算机的应用方兴未艾,(全国通用版)18版高考语文大一轮复习第2周基础组合练4...

热门文章

  1. 3D视觉之深度相机方案
  2. Java的两种分页实现
  3. 在mac上安装md5命令
  4. 矩阵范数,向量范数,奇异值有什么用?
  5. Scrapy框架快速执行cmd命令:‘scrapy crawl qsbk_spider’
  6. Java 小练习(简单)—合集
  7. PATH,PYTHONPATH 与sys.path的区别
  8. 基于Spark的新闻推荐系统,包含爬虫项目、web网站以及spark推荐系统
  9. 医院导诊图怎么做,专业便捷、低成本的室内电子地图绘制平台!
  10. 2022软件测试工程师的简历怎么写?