解决go在函数退出后子协程的退出问题


Posted in Golang onApril 30, 2021

该问题来源于自己在读fabric源码时,看到的一个测试代码,在一个函数中启用协程,然后该函数退出了,由于平常没有这样处理过,以及受原有c++函数域的影响,认为函数退出,子协程应该也退出了呀。

这其实是自己对go协程的理解不到位引起的,go的协程作用域不是在某个函数中的,当然,如果那个函数是main函数,就符合要求了。

该代码为solo算法的测试代码:

func goWithWait(target func()) *waitableGo {
	wg := &waitableGo{
		done: make(chan struct{}),
	}
	go func() {
		target()//该协程会阻塞在这
		close(wg.done)//用来对外通知
	}()
	//外边结束,里边还不结束吗?
	return wg
}
// This test checks that if consenter is halted before a timer fires, nothing is actually written.
func TestHaltBeforeTimeout(t *testing.T) {
	batchTimeout, _ := time.ParseDuration("1ms")
	//support的构造还不清楚
	support := &mockmultichannel.ConsenterSupport{
		Blocks:          make(chan *cb.Block),
		BlockCutterVal:  mockblockcutter.NewReceiver(),
		SharedConfigVal: &mockconfig.Orderer{BatchTimeoutVal: batchTimeout},
	}
	defer close(support.BlockCutterVal.Block)
	bs := newChain(support)
	//bs.main是solo算法的启动函数,是个死循环,处理函数
	wg := goWithWait(bs.main)
	defer bs.Halt()//中止
	syncQueueMessage(testMessage, bs, support.BlockCutterVal)
	bs.Halt()
	select {
	case <-support.Blocks:
		t.Fatalf("Expected no invocations of Append")
	case <-wg.done:
	}
}

遇到该问题后,我写了几个测试:

单纯的函数退出,是不会影响协程的

package main
import "fmt"
var ch chan int
func test() int {
 ch = make(chan int)
 go func() {
  for {
   fmt.Println(<-ch)
   fmt.Println("hello")
  }
  fmt.Println("aaaa")
 }()
 //不阻塞,那go func()不会异常退出吗?
 //协程并不是函数,不会因为这个函数的退出而退出
 //test()启动一个deadloop子协程,这个会在主协程main结束后被强制退出
 return 0
}
func main() {
 c := test()
 ch <- 10
 fmt.Println("c", c)
}

我经常在main里边直接写协程的测试demo,main退出会结束主协程,之后会强制结束子协程,一般不会遇到上述在普通函数退出的问题,也没仔细思考,所以分析源码时有点困惑。

子协程启动子协程,父协程的退出,并没有影响到子协程

liudeMacBook-Pro:~ liu$ cat tmp.go 
package main
import (
	"fmt"
	"time"
)
func test() {
	go func() { //父协程
		defer func() {
			fmt.Println("exit dad")
		}()
		go func() { //子协程
			defer func() {
				fmt.Println("exit kid")
			}()
		}()
	}()
}
func main() {
	test()
	time.Sleep(time.Second)
}
liudeMacBook-Pro:~ liu$ go run tmp.go 
exit dad
exit kid

补充:golang中父子协程生命周期问题,以及通过context优雅关闭子协程

背景

上次基于mysql实现分布式锁,今天经过测试发现问题,主要是协程不断获取锁的逻辑存在问题,因为获取锁的协程挂掉之后,但其新生成的用来不断更新锁的协程并不会退出,导致锁一直不能被释放,究其原因如下

原因

通过下面代码即可说明

fmt.Println("main 函数 开始...")
	go func() {
		fmt.Println("父 协程 开始...")
		go func() {
			for {
				fmt.Println("子 协程 执行中...")
				timer := time.NewTimer(time.Second * 2)
				<-timer.C
			}
		}()
		time.Sleep(time.Second*5)
		fmt.Println("父 协程 退出...")
	}()
	time.Sleep(time.Second*10)
	fmt.Println("main 函数 退出")

main 函数 开始...

父 协程 开始...

子 协程 执行中...

子 协程 执行中...

子 协程 执行中...

父 协程 退出...

子 协程 执行中...

子 协程 执行中...

main 函数 退出

由此可以看出:

main 函数退出,所有协程退出

协程无父子关系,即在父协程开启新的协程,若父协程退出,不影响子协程

解决方式

通过context上下文来解决,当然也可以通过channel管道来解决,context解决方式如下:

fmt.Println("main 函数 开始...")
	go func() {
		ctx, cancel := context.WithCancel(context.Background())
		defer cancel()
		fmt.Println("父 协程 开始...")
		go func(ctx context.Context) {
			for {
				for {
					select {
					case <-ctx.Done():
						fmt.Println("子 协程 接受停止信号...")
						return
					default:
						fmt.Println("子 协程 执行中...")
						timer := time.NewTimer(time.Second * 2)
						<-timer.C
					}
				}
			}
		}(ctx)
		time.Sleep(time.Second*5)
		fmt.Println("父 协程 退出...")
	}()
	time.Sleep(time.Second*10)
	fmt.Println("main 函数 退出")

main 函数 开始...

父 协程 开始...

子 协程 执行中...

子 协程 执行中...

子 协程 执行中...

父 协程 退出...

子 协程 接受停止信号...

main 函数 退出

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。如有错误或未考虑完全的地方,望不吝赐教。

Golang 相关文章推荐
基于go interface{}==nil 的几种坑及原理分析
Apr 24 Golang
Go语言-为什么返回值为接口类型,却返回结构体
Apr 24 Golang
用golang如何替换某个文件中的字符串
Apr 25 Golang
Go语言中的UTF-8实现
Apr 26 Golang
Go语言切片前或中间插入项与内置copy()函数详解
Apr 27 Golang
golang 实现两个结构体复制字段
Apr 28 Golang
Go语言中break label与goto label的区别
Apr 28 Golang
基于Go Int转string几种方式性能测试
Apr 28 Golang
浅谈golang package中init方法的多处定义及运行顺序问题
May 06 Golang
解决Goland 同一个package中函数互相调用的问题
May 06 Golang
深入理解go slice结构
Sep 15 Golang
Golang 结构体数据集合
Apr 22 Golang
Go语言 go程释放操作(退出/销毁)
golang DNS服务器的简单实现操作
golang slice元素去重操作
Apr 30 #Golang
Golang中interface{}转为数组的操作
Apr 30 #Golang
解决Go gorm踩过的坑
Apr 30 #Golang
Golang 如何实现函数的任意类型传参
Apr 29 #Golang
解决Golang time.Parse和time.Format的时区问题
Apr 29 #Golang
You might like
PHP 多进程 解决难题
2009/06/22 PHP
php程序之die调试法 快速解决错误
2009/09/17 PHP
php获取给定日期相差天数的方法分析
2017/02/20 PHP
thinkPHP5.0框架配置格式、加载解析与读取方法
2017/03/17 PHP
增强的 JavaScript 的 trim 函数的代码
2007/08/13 Javascript
Javascript 入门基础学习
2010/03/10 Javascript
picChange 图片切换特效的函数代码
2010/05/06 Javascript
在图片上显示左右箭头类似翻页的代码
2013/03/04 Javascript
js使用eval解析json实例与注意事项分享
2014/01/18 Javascript
删除javascript中注释语句的正则表达式
2014/06/11 Javascript
Javascript中拼接大量字符串的方法
2015/02/05 Javascript
JavaScript简介
2015/02/15 Javascript
jquery实现标签支持图文排列带上下箭头按钮的选项卡
2015/03/14 Javascript
JS实现可关闭的对联广告效果代码
2015/09/14 Javascript
一不小心就做错的JS闭包面试题
2015/11/25 Javascript
使用Object.defineProperty实现简单的js双向绑定
2016/04/15 Javascript
ArtEditor富文本编辑器增加表单提交功能
2016/04/18 Javascript
Javascript自执行匿名函数(function() { })()的原理浅析
2016/05/15 Javascript
利用Javascript裁剪图片并存储的简单实现
2017/03/13 Javascript
jquery根据name取得select选中的值实例(超简单)
2018/01/25 jQuery
详解小程序rich-text对富文本支持方案
2018/11/28 Javascript
基于Bootstrap和JQuery实现动态打开和关闭tab页的实例代码
2019/06/10 jQuery
JS实现简单省市二级联动
2019/11/27 Javascript
如何HttpServletRequest文件对象并储存
2020/08/14 Javascript
Vue实现点击导航栏当前标签后变色功能
2020/08/19 Javascript
HTML元素拖拽功能实现的完整实例
2020/12/04 Javascript
python sqlobject(mysql)中文乱码解决方法
2008/11/14 Python
Python模块文件结构代码详解
2018/02/03 Python
浅谈Python 多进程默认不能共享全局变量的问题
2019/01/11 Python
django框架创建应用操作示例
2019/09/26 Python
Tensorflow使用Anaconda、pycharm安装记录
2020/07/29 Python
Html5基于canvas实现电子签名并生成PDF文档
2020/12/07 HTML / CSS
运动服饰每月订阅盒:Ellie
2018/04/29 全球购物
美国高档帽子网上商店:Hats.com
2018/08/09 全球购物
LN-CC美国:伦敦时尚生活的缩影
2019/02/19 全球购物
2016廉政教育学习心得体会
2016/01/25 职场文书