解决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 实现超大文件读取的两种方法
Apr 27 Golang
golang 生成对应的数据表struct定义操作
Apr 28 Golang
解决goland 导入项目后import里的包报红问题
May 06 Golang
解决golang 关于全局变量的坑
May 06 Golang
基于Golang 高并发问题的解决方案
May 08 Golang
golang fmt格式“占位符”的实例用法详解
Jul 04 Golang
GO语言异常处理分析 err接口及defer延迟
Apr 14 Golang
Golang 对es的操作实例
Apr 20 Golang
GoFrame gredis缓存DoVar Conn连接对象 自动序列化GoFrame gredisDo/DoVar方法Conn连接对象自动序列化/反序列化总结
Jun 14 Golang
Golang gRPC HTTP协议转换示例
Jun 16 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
AM/FM收音机的安装与调试
2021/03/02 无线电
thinkPHP框架实现的简单计算器示例
2018/12/07 PHP
JS类定义原型方法的两种实现的区别评论很多
2007/09/12 Javascript
利用javascript移动div层-javascript 拖动层
2009/03/22 Javascript
javascript Array.prototype.slice使用说明
2010/10/11 Javascript
Jquery动态改变图片IMG的src地址示例
2013/06/25 Javascript
jQuery 获取和设置select下拉框的值实现代码
2013/11/08 Javascript
在JS数组特定索引处指定位置插入元素的技巧
2014/08/24 Javascript
JavaScript如何自定义trim方法
2015/07/28 Javascript
JS 拼凑字符串的简单实例
2016/09/02 Javascript
jQuery Ajax使用FormData对象上传文件的方法
2016/09/07 Javascript
Vue.js在使用中的一些注意知识点
2017/04/29 Javascript
简单明了区分escape、encodeURI和encodeURIComponent
2018/05/26 Javascript
vue移动端下拉刷新和上拉加载的实现代码
2018/09/08 Javascript
vue 翻页组件vue-flip-page效果
2020/02/05 Javascript
基于vue项目设置resolves.alias: '@'路径并适配webstorm
2020/12/02 Vue.js
vuex Module将 store 分割成模块的操作
2020/12/07 Vue.js
[02:10]DOTA2亚洲邀请赛 EG战队出场宣传片
2015/02/07 DOTA
Python和C/C++交互的几种方法总结
2017/05/11 Python
Python 异常处理的实例详解
2017/09/11 Python
numpy中索引和切片详解
2017/12/15 Python
Python操作mysql数据库实现增删查改功能的方法
2018/01/15 Python
Django-Rest-Framework 权限管理源码浅析(小结)
2018/11/12 Python
python读取目录下最新的文件夹方法
2018/12/24 Python
python统计中文字符数量的两种方法
2019/01/31 Python
离线状态下在jupyter notebook中使用plotly实例
2020/04/24 Python
django 连接数据库出现1045错误的解决方式
2020/05/14 Python
pytorch判断是否cuda 判断变量类型方式
2020/06/23 Python
Python 如何调试程序崩溃错误
2020/08/03 Python
Marriott国际:万豪国际酒店查询预订
2017/09/25 全球购物
给酒店员工的表扬信
2014/01/11 职场文书
七年级地理教学反思
2014/01/26 职场文书
民主评议政风行风整改方案
2014/09/17 职场文书
家长通知书家长意见
2015/06/03 职场文书
Nginx设置日志打印post请求参数的方法
2021/03/31 Servers
MySQL中in和exists区别详解
2021/06/03 MySQL