解决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 相关文章推荐
golang interface判断为空nil的实现代码
Apr 24 Golang
Go语言-为什么返回值为接口类型,却返回结构体
Apr 24 Golang
golang正则之命名分组方式
Apr 25 Golang
解决Golang中goroutine执行速度的问题
May 02 Golang
go语言中fallthrough的用法说明
May 06 Golang
Golang 获取文件md5校验的方法以及效率对比
May 08 Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 Golang
Go遍历struct,map,slice的实现
Jun 13 Golang
Go 语言下基于Redis分布式锁的实现方式
Jun 28 Golang
基于Go语言构建RESTful API服务
Jul 25 Golang
一文搞懂Golang 时间和日期相关函数
Dec 06 Golang
golang定时器
Apr 14 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.MVC的模板标签系统(五)
2006/09/05 PHP
php正则校验用户名介绍
2008/07/19 PHP
PHP 如何利用phpexcel导入数据库
2013/08/24 PHP
php判断手机访问还是电脑访问示例分享
2014/01/20 PHP
JQuery下关于$.Ready()的分析
2009/12/13 Javascript
jquery下异步提交表单 异步跨域提交表单
2010/11/17 Javascript
Jquery+Ajax+PHP+MySQL实现分类列表管理(上)
2015/10/28 Javascript
AngularJS 日期格式化详解
2015/12/23 Javascript
详解nodejs爬虫程序解决gbk等中文编码问题
2017/04/06 NodeJs
jQuery插件ImgAreaSelect实现头像上传预览和裁剪功能实例讲解一
2017/05/26 jQuery
微信小程序实现多个按钮toggle功能的实例
2017/06/13 Javascript
基于AngularJS的拖拽文件上传的实例代码
2017/07/15 Javascript
JavaScript实现带有子菜单和控件的slider轮播图效果
2017/11/01 Javascript
解决angularjs service中依赖注入$scope报错的问题
2018/10/02 Javascript
jQuery事件多次绑定与解绑问题实例分析
2019/02/19 jQuery
vue指令v-html使用过滤器filters功能实例
2019/10/25 Javascript
详解Vue 单文件组件的三种写法
2020/02/19 Javascript
[02:38]2018DOTA2亚洲邀请赛赛前采访-VGJ.T
2018/04/03 DOTA
[48:56]2018DOTA2亚洲邀请赛 3.31 小组赛 A组 VG vs KG
2018/03/31 DOTA
python提取内容关键词的方法
2015/03/16 Python
Python中编写ORM框架的入门指引
2015/04/29 Python
Python 实现简单的电话本功能
2015/08/09 Python
Python面向对象之类的定义与继承用法示例
2019/01/14 Python
PyQt5实现简易电子词典
2019/06/25 Python
加拿大女装网上购物:Reitmans
2016/10/20 全球购物
美国高级工作服品牌:Carhartt
2018/01/25 全球购物
工程管理造价应届生求职信
2013/11/13 职场文书
校园标语大全
2014/06/19 职场文书
群众路线个人对照检查材料
2014/09/23 职场文书
员工开除通知书
2015/04/25 职场文书
社区法制宣传日活动总结
2015/05/05 职场文书
幼儿园中班教学反思
2016/03/03 职场文书
用Python写一个简易版弹球游戏
2021/04/13 Python
python中requests库+xpath+lxml简单使用
2021/04/29 Python
「海贼王」112.9万粉丝纪念图标公布
2022/03/21 日漫
MySQL分区以及建索引的方法总结
2022/04/13 MySQL