goroutine的退出机制

大家都知道goroutine是Go语言并发的利器,通过goroutine我们可以很容易的编写高并发的程序。但是goroutine设计的退出机制是由goroutine自己退出,不能在外部强制结束一个正在执行的goroutine(只有一种情况正在运行的goroutine会因为其他goroutine的结束被终止,就是main函数退出或程序停止执行)。关于goroutine为什么要设计成这样的退出机制,改天再po两篇译文上来(别人已经写得很清楚了,我想我应该不需要做额外的总结了)。

但是最近遇到一个坑,就是我有很多可并发的一次性事务。对每一个事务我都起一个goroutine来执行。正常情况下事务执行完毕, goroutine就退出。没有循环,没有复杂的逻辑控制,顺序执行就完事儿了。但是坑就坑在顺序执行上了,如果顺序执行过程中因为某个原因block了,比如读IO,获取连接,或者就是一个很耗时的计算,我想为这个goroutine设置一个超时退出,或者异常退出,却因为goroutine的这种机制我没法为这种类型的goroutine根据超时、异常执行强制退出的操作。

所以乘机整理了一下几种能够让一个goroutine退出的机制。

main 退出

这个没有什么好具体说的,main是Go程序的主入口,main函数退出基本意味着你代码执行的结束。进程都退出了,所有它占有的资源都会还给操作系统,所以还结束的goroutines也没什么好玩儿的了。

通过channel通知退出

这个最主要的goroutine退出方式。goroutine虽然不能强制结束另外一个goroutine,但是它是它可以通过channel通知另外一个goroutine你的表演该结束了。常用的方法到处都可以看到,这里也不详细说明了,直接上一个示例:

下面的示例中起了一个goroutine执行cancelByChannel,但是在起它之前还通过time.After返回了一个time.Time类型的channel,该channel上在定时超时时会发送一个当前时间数据。`cancelByChannel每隔1s会检查这个channel上是否有数据接收,如果有数据则退出goroutine,如果没有信号接收就在连接上发送一条数据。所以下面这段代码在运行10s发送10条消息后将退出。

程序起起来后,另开一个终端执行nc localhost:8000(Linux上)或nc localhost 8000(mac 上)可以看到程序执行情况。

package mainimport ("context""fmt""io""net""sync""time"
)func cancelByChannel(c net.Conn, quit <-chan time.Time, wg *sync.WaitGroup) {defer c.Close()defer wg.Done()for {select {case <-quit:fmt.Println("cancel goroutine by channel!")returndefault:_, err := io.WriteString(c, "hello cancelByChannel")if err != nil {return}time.Sleep(1 * time.Second)}}
}func main() {listener, err := net.Listen("tcp", "localhost:8000")if err != nil {fmt.Println(err)return}conn, err := listener.Accept()if err != nil {fmt.Println(err)return}wg := sync.WaitGroup{}wg.Add(1)quit := time.After(time.Second * 10)go cancelByChannel(conn, quit, &wg)wg.Wait()
}

通过context通知goroutine退出

通过channel通知goroutine退出还有一个更好的方法就是使用context。关于Context的详细信息可以参考前面的文章Go并发模式: Context。它本质还是接收一个channel数据,只是是通过ctx.Done()获取。将上面的示例稍作修改就可以用起来了。

func cancelByContext(ctx context.Context, c net.Conn, wg *sync.WaitGroup) {defer c.Close()defer wg.Done()for {select {case <-ctx.Done():fmt.Println("cancel goroutine by context:", ctx.Err())returndefault:_, err := io.WriteString(c, "hello cancelByContext")if err != nil {return}time.Sleep(1 * time.Second)}}
}

main函数中将这两行代码:

    quit := time.After(time.Second * 10)go cancelByChannel(conn, quit, &wg)

使用下面几行替换:

    ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)defer cancel()go cancelByContext(ctx, conn, &wg)

panic 退出

这种方法有点走邪门歪道了,这个需要依赖一些标准库和第三方库的设计机制,没有处理好可能会出现一些你完全意料不到的结果,慎用。这种方法涉及到的知识点包括,defer, panic, recover,详细可参见前面的译文defer, panic, recover。

这种方式能解决部分我在本文开头提到的问题。比如如果因为某次IO block了(比如一次数据库inert事务)等。这一类事务比如数据库操作,文件操作的步骤通常是:建立连接(或打开文件),然后执行读写操作,读写完成后关闭连接(或文件)。假如因为某种原因我们在读写操作那一步block了。我们又不希望这个goroutine一直block在那儿,或者我们需要这个goroutine在指定时间内完成。这时我们期望goroutine自己结束并退出可能就有点不现实了。

这个时候加入我们关闭文件或链接会怎么样?我也不知道,这样看你网络操作,文件操作所使用的标准库或者第三方库的实现了。但是,通常情况下链接或文件关闭后,你的读写操作要么会立即抛出一个panic,要么就是立即返回一个错误了。注意,这里说的是通常情况,不是所有情况,还有具体是抛出panic还是error,这些都需要根据你自己的实际情况具体分析。

所以这里主要针对的是panic和error的退出方式,看下面的模拟示例。(作者遇到的坑是block在一次mongo的写操作上,由于问题不太好表现,这里没有使用该示例,而是基于前面的示例做了一些修改。)这里由于断开net.Conn的连接,通过io.Writer往连接上只是返回了一个error,并不能成功模拟recover panic的方式。所以示例只能作为这种实现方式的参考,另外也说明了这种方式的不确定性。* 所以再强调一遍,一定要慎用。*

func cancelByPanic(c net.Conn, wg *sync.WaitGroup) {defer func() {if err := recover(); err != nil {fmt.Println("cancel goroutine by context:", err)}}()defer wg.Done()for {_, err := io.WriteString(c, "hello cancelByPanic")if err != nil {fmt.Println(err)return}time.Sleep(1 * time.Second)}
}

上面函数中defer函数中使用了recover来捕获panic error并从panic中拿回控制权,确保程序不会再panic展开到goroutine调用栈顶部后崩溃。

main函数也要做相应的更改,还需要起一个额外的goroutine来根据相应的退出机制关闭连接。示例中设置的是超时。超时后连接关闭,io.WriteString()将返回一个错误,然后退出goroutine.

    go func(ctx context.Context, conn net.Conn, wg *sync.WaitGroup) {defer wg.Done()for {select {case <-ctx.Done():fmt.Println("---close the connection outside!")conn.Close()returndefault:}}}(ctx, conn, &wg)wg.Add(1)go cancelByPanic(conn, &wg)

等它自己退出:)

最后,还有一种情况也可能是大家经常遇到的,就是本文开头提到的你的goroutine可能只是执行一个计算,但是这个计算执行的时间有点长。对于这种方式,貌似如果你不打算改你的设计换一种方式执行程序的话,就只有等它自己结束了。

下面也是一个示例,这个示例只是根据一个初始值计算进行累减数求和。本例中使用简单的递归求和的方式,随着初始值的变大,计算过程会越来越慢。

func slowCal(fac int) int {if fac < 2 {return fac}return slowCal(fac-1) + slowCal(fac-2)
}func cancelByWait(wg *sync.WaitGroup) {defer wg.Done()start := time.Now()result := slowCal(50)dur := time.Since(start)fmt.Println("slow goroutine done:", result, dur)
}

main 函数中直接执行go cancelByWait即可。

这种方式的改进

这个示例还有很大的改进空间,这里也不深入展开了。只简单的提两点,读者可以自己下去尝试下。当然这个例子也很简单,也不用花时间去写代码,想想应该就可以了:)。

  1. 可以通过优化算法,以及修改并发方式提高计算速度。

  2. 这个示例也是可以引入context或channel来通知计算超时退出的,如果你不想要计算结果的话。

总结

由于Goroutine被设计为只能自己退出,而不能强制退出。在实际使用中,我们可能会因为某些原因被block在Goroutines里面,或由于设计缺陷导致一些Goroutines执行很长的时间。只是基于一些其他语言的经验,我们可能会期望有一种外部机制能够强制结束一个Goroutines。但是这就是Go和Goroutine,它的目的就是要提供一种轻量的,简单的并发方式。保证它这个特性的基础也决定了我们不能用外部方式强制关闭一个Goroutines(额外post译文或博文说明这个问题,此文不深入展开)。所以当你遇到这种情况的时候,你可能需要考虑你的设计是不是足够的Go style,或者你对一些外部依赖是否足够了解了。

goroutine退出方式的总结相关推荐

  1. golang goroutine 退出方法

    目录 传统方式 单个goroutine退出 多个goroutine退出 Context包 控制退出 context.WithTimeout 退出 context.WithCanel 退出 contex ...

  2. python退出程序-Python程序退出方式小结

    对于如何结束一个Python程序或者用Python操作去结束一个进程等,Python本身给出了好几种方法,而这些方式也存在着一些区别,对相关的几种方法看了并实践了下,同时也记录下. 参考: Pytho ...

  3. Python程序退出方式小结(亲测)

    这篇文章主要介绍了Python程序退出方式小结,具有一定参考价值,需要的朋友可以了解下. 对于如何结束一个Python程序或者用Python操作去结束一个进程等,Python本身给出了好几种方法,而这 ...

  4. 微信小程序退出按钮退出方式

    微信小程序退出按钮退出方式 1,只能跳转到 tabBar配置的页面 wx.switchTab({url:''../xxx/xxx}) 2.返回上一级页面 (delta:返回的页面数,如果delta大于 ...

  5. python的两种退出方式

    os._exit() vs sys.exit() 转自: http://www.cnblogs.com/gaott/archive/2013/04/12/3016355.html 概述 python的 ...

  6. Python程序退出方式小结

    Python程序退出方式小结 这篇文章主要介绍了Python程序退出方式小结,具有一定参考价值,需要的朋友可以了解下. 对于如何结束一个Python程序或者用Python操作去结束一个进程等,Pyth ...

  7. python的退出方式

    os._exit() vs sys.exit() 转自:http://www.cnblogs.com/gaott/archive/2013/04/12/3016355.html 概述 python的程 ...

  8. MUI框架 APP手机退出方式

    1.手机返回键监听 点击两次退出系统 mui.oldback = mui.back; var clickNum = 0; mui.back = function(event){    clickNum ...

  9. python sys.exit_Python程序退出方式(sys.exit() os._exit() os.kill() os.popen(...))

    对于如何结束一个Python程序或者用Python操作去结束一个进程等,Python本身给出了好几种方法,而这些方式也存在着一些区别,对相关的几种方法看了并实践了下,同时也记录下. 参考: Pytho ...

最新文章

  1. 修改IDEA运行jsp文件的时候浏览器地址栏的虚拟访问路径网址
  2. 请问大数据有没有速成的方法?嗯 真的没有
  3. opentracing
  4. 去分库分表的亿级数据NewSQL实践之旅
  5. 从iOS应用中,启动一个Unity App
  6. linux本地yum源与软件包管理,【Linux系统中的】本地yum源的搭建与使用yum源进行软件的下载...
  7. Java处理文件BOM头的方式推荐
  8. Pytorch出现Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
  9. DOS的一个小工具 LOIC
  10. 基于微信小程序的在线考试系统【毕业设计源码】
  11. Linux 多点电容触摸屏
  12. FRABA绝对值编码器 OCD58-EA00B-1213-S0
  13. 淘宝标题怎么写才能具有高权重
  14. 古龙冰洞超级计算机指令,龙族幻想古龙冰洞异闻攻略 古龙冰洞指令介绍
  15. vscode远程连接服务器失败的问题
  16. 【042】904. 水果成篮[滑动窗口]
  17. 关于企业服务总线ESB
  18. Hack The Box-Fawn
  19. 小程序Cannot read property 'elem' of undefined
  20. Arduino uno入门学习(1)

热门文章

  1. 三菱PLC与第三方设备TCP通讯_不用在PLC内编程,快速实现西门子与欧姆龙、三菱等品牌的PLC之间实时通讯...
  2. Windows 7下直接开启AHCI
  3. 用友畅捷通系列软件运行单据列表查询时报“错误‘6’ 溢出”错误!
  4. 在线html5视频播放器,分享10款最棒的免费HTML5视频播放器
  5. ACL 通配符掩码 匹配的范围计算及理解
  6. flask实现文件简易服务器,可根据链接上传下载
  7. 大数据治理解决方案PPT
  8. 【CV】第 8 章:语义分割和神经风格迁移
  9. 如何用 Python 做一个简单的翻译工具?
  10. 使用docker创建fdfs并使用