回答我,停止 Goroutine 有几种方法?

大家好,我是煎鱼。

协程(goroutine)作为 Go 语言的扛把子,经常在各种 Go 工程项目中频繁露面,甚至有人会为了用 goroutine 而强行用他。

在 Go 工程师的面试中,也绕不开他,会有人问 ”如何停止一个 goroutine?”,一下子就把话题范围扩大了,这是一个涉及多个知识点的话题,能进一步深入问。

为此,今天煎鱼就带大家了解一下停止 goroutine 的方法!

goroutine 案例

在日常的工作中,我们常会有这样的 Go 代码,go 关键字一把搜起一个 goroutine:

func main() { 
 ch := make(chan string, 6)
 go func() {
  for {
   ch <- '脑子进煎鱼了'
  }
 }()
}

初入 goroutine 大门的开发者可能就完事了,但跑一段时间后,他就可能会遇到一些问题,苦苦排查...

像是:当 goroutine 内的任务,运行的太久,又或是卡死了...就会一直阻塞在系统中,变成 goroutine 泄露,或是间接造成资源暴涨,会带来许多的问题。

如何在停止 goroutine,就成了一门必修技能了,不懂就没法用好 goroutine。

关闭 channel

第一种方法,就是借助 channel 的 close 机制来完成对 goroutine 的精确控制。

代码如下:

func main() { ch := make(chan string, 6) go func() {  for {   v, ok := <-ch   if !ok {    fmt.Println('结束')    return   }   fmt.Println(v)  } }()

 ch <- '煎鱼还没进锅里...' ch <- '煎鱼进脑子里了!' close(ch) time.Sleep(time.Second)}

在 Go 语言的 channel 中,channel 接受数据有两种方法:

msg := <-ch
msg, ok := <-ch

这两种方式对应着不同的 runtime 方法,我们可以利用其第二个参数进行判别,当关闭 channel 时,就根据其返回结果跳出。

另外我们也可以利用 for range 的特性:

 go func() {  for {   for v := range ch {    fmt.Println(v)   }  } }()

其会一直循环遍历通道 ch,直到其关闭为止,是颇为常见的一种用法。

定期轮询 channel

第二种方法,是更为精细的方法,其结合了第一种方法和类似信号量的处理方式。

代码如下:

func main() {
 ch := make(chan string, 6)
 done := make(chan struct{})
 go func() {
  for {
   select {
   case ch <- '脑子进煎鱼了':
   case <-done:
    close(ch)
    return
   }
   time.Sleep(100 * time.Millisecond)
  }
 }()

go func() {
  time.Sleep(3 * time.Second)
  done <- struct{}{}
 }()

for i := range ch {
  fmt.Println('接收到的值: ', i)
 }

fmt.Println('结束')
}

在上述代码中,我们声明了变量 done,其类型为 channel,用于作为信号量处理 goroutine 的关闭。

而 goroutine 的关闭是不知道什么时候发生的,因此在 Go 语言中会利用 for-loop 结合 select 关键字进行监听,再进行完毕相关的业务处理后,再调用 close 方法正式关闭 channel。

若程序逻辑比较简单结构化,也可以不调用 close 方法,因为 goroutine 会自然结束,也就不需要手动关闭了。

使用 context

第三种方法,可以借助 Go 语言的上下文(context)来做 goroutine 的控制和关闭。

代码如下:

func main() { ch := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background())

 go func(ctx context.Context) {  for {   select {   case <-ctx.Done():    ch <- struct{}{}    return   default:    fmt.Println('煎鱼还没到锅里...')   }

   time.Sleep(500 * time.Millisecond)  } }(ctx)

 go func() {  time.Sleep(3 * time.Second)  cancel() }()

 <-ch fmt.Println('结束')}

在 context 中,我们可以借助 ctx.Done 获取一个只读的 channel,类型为结构体。可用于识别当前 channel 是否已经被关闭,其原因可能是到期,也可能是被取消了。

因此 context 对于跨 goroutine 控制有自己的灵活之处,可以调用 context.WithTimeout 来根据时间控制,也可以自己主动地调用 cancel 方法来手动关闭。

干掉另外一个 goroutine

在了解了停止 goroutine 的 3 种经典方法后,又有小伙伴提出了新的想法。就是 “我想在 goroutineA 里去停止 goroutineB,有办法吗?

答案是不能,因为在 Go 语言中,goroutine 只能自己主动退出,一般通过 channel 来控制,不能被外界的其他 goroutine 关闭或干掉,也没有 goroutine 句柄的显式概念。

go/issues/32610

在 Go issues 中也有人提过类似问题,Dave Cheney 给出了一些思考:

  • 如果一个 goroutine 被强行停止了,它所拥有的资源会发生什么?堆栈被解开了吗?defer 是否被执行?
    • 如果执行 defer,该 goroutine 可能可以继续无限期地生存下去。
    • 如果不执行 defer,该 goroutine 原本的应用程序系统设计逻辑将会被破坏,这肯定不合理。
  • 如果允许强制停止 goroutine,是要释放所有东西,还是直接把它从调度器中踢出去,你想通过此解决什么问题?

这都是值得深思的,另外一旦放开这种限制。作为程序员,你维护代码。很有可能就不知道 goroutine 的句柄被传到了哪里,又是在何时何地被人莫名其妙关闭,非常糟糕...

总结

在今天这篇文章中,我们介绍了在 Go 语言中停止 goroutine 的三大经典方法(channel、context,channel+context)和其背后的使用原理。

同时针对 goroutine 不可以跨 goroutine 强制停止的原因进行了分析。其实 goroutine 的设计就是这样的,包括像 goroutine+panic+recover 的设计也是遵循这个原理,因此也有的 Go 开发者总是会误以为跨 goroutine 能有 recover 接住...

记住,在 Go 语言中每一个 goroutine 都需要自己承担自己的任何责任,这是基本原则。

(你已经是个成熟的 goroutine 了...)

脑子进煎鱼了

分享计算机基础、Go 语言、微服务架构和系统设计;著有图书《Go 语言编程之旅》。
127篇原创内容
公众号

关注煎鱼,吸取他的知识 👆

你好,我是煎鱼。高一折腾过前端,参加过国赛拿了奖,大学搞过 PHP。现在整 Go,在公司负责微服务架构等相关工作推进和研发。

从大学开始靠自己赚生活费和学费,到出版 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,记得点赞!

(0)

相关推荐

  • 从 bug 中学:六大开源项目告诉你 go 并发编程的那些坑

    作者:richardyao,腾讯 CSIG 后台开发工程师 并发编程中,go不仅仅支持传统的通过共享内存的方式来通信,更推崇通过channel来传递消息,这种新的并发编程模型会出现不同于以往的bug. ...

  • 信道:如何通过信道完成Go程(goroutine)同步?

    中文译为信道,英文是Channel,发音为[ˈtʃænl]),在Go语言中简写为chan.chan是Go语言的基本数据类型之一,也是Go语言中为难不多三个使用make关键字进行初始化的三个类型(信道. ...

  • recover.panic.defer.2021.03.03

    Defer, Panic, and Recover 在 Go 语言中,recover 和 panic 的关系是什么? 我们先看一个基础的例子,在 main 方法体中启动一个协程,在协程内部主动调用 p ...

  • Go之定时器的使用

    一.背景介绍 笔者最近在使用Go的定时器,发现Go提供的time包里面,按照下面几种场景做了区分,并分别提供一些API支持.主要场景如下所示: 1.超时一次之后,就不再使用的定时器,time.Afte ...

  • 在Go中,你犯过这些错误吗

    Go语言中文网 今天 以下文章来源于吴亲强的深夜食堂 ,作者吴亲库里 吴亲强的深夜食堂关注一些奇奇怪怪的设计,分享一些有有趣趣的生活 迭代器变量上使用 goroutine 这算高频吧. package ...

  • 一看就懂系列之Golang的goroutine和通道

    https://blog.csdn.net/u011957758/article/details/81159481 前言 如果说php是最好的语言,那么golang就是最并发的语言. 支持golang ...

  • 领导问:“车不错,多少钱买的?”聪明人这3种方法回答,不尴尬

    小王在职场中是一个积极的员工,虽然在这个新单位并没有来几天,但是小王性格很放得开,所以在很多时候和别人相处得好. 小王自己也是很满意现在的工作环境,于是自己工作起来也更加有动力了.不过小王还是想着什么 ...

  • 建议你不要做一个“想太多”的人——4种方法,停止你的过度思考

    生活里,有些表现,虽然算不上是一种病症,可是给人带来的困扰,可能并不亚于一场"疾病". 杨绛老师说过一句话. 你的问题在于读书太少,而想得太多. 敏感.多疑.过虑给一个人带来的,往 ...

  • UC头条:建议你不要做一个“想太多”的人——4种方法, 停止你的过度思考

    生活里,有些表现,虽然算不上是一种病症,可是给人带来的困扰,可能并不亚于一场"疾病". 杨绛老师说过一句话. 你的问题在于读书太少,而想得太多. 敏感.多疑.过虑给一个人带来的,往 ...

  • 停止口腔呼吸的6种方法

    停止口腔呼吸的方法 治疗结构性问题如果你有任何 避免过敏原来治疗鼻塞 通过肌功能疗法改善你的气道肌肉 对你的呼吸模式进行生物反馈 使用口腔设备:下颚带和前庭盾 口腔呼吸通常始于对鼻腔气道阻塞的反应.避 ...

  • 社保没交满15年的有救了!新规下,这5种方法一定要知道!

    在社保中,大家除了关注医保外,对养老金的关注度也很高,毕竟养老金是很多退休老人的主要生活来源. 我们都知道,只有当退休时缴纳了至少15年社保,才能拿到养老金.根据不同地区的不同规定,需要缴纳15年.2 ...

  • 健康早知道|老花眼年轻化 这几种方法让你彻底摘掉老花镜

    健康早知道|老花眼年轻化 这几种方法让你彻底摘掉老花镜

  • 书法这样写,越写越有味!(10种方法分享)

    在美学角度而言,书法是一种视觉造形艺术,讲究变化与造势.下面,我们总结书法10种变化,悟懂和实践这些变化,越写越清爽.越有味! 大 小 变 化 三个同样大小的字,笔划的粗细与多少差不多的,若相连在一起 ...

  • 危机时期管理业务的7种方法

    对于我们所有人来说,疫情是不确定的时期,现金流量很少的小型企业主尤其感到紧缩.为了帮助你确定如何在诸如冠状病毒等危机时期管理业务,我们与世界领先的管理专家进行沟通,以下是一些要点,Azides博士为你 ...

  • 不确定时期保持稳定招聘的5种方法

    在疫情这些不稳定的时期, 你如何才能维持稳定的招聘流程,并继续为候选人提供与目标和价值观一致的优质体验? 通过数字化,诚实的沟通,发展员工以及大量急需的同理心,这仍然是可能实现的,在不确定时期保持稳定 ...