解决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语言操作数据库及其常规操作的示例代码
Apr 21 Golang
Golang 使用Map实现去重与set的功能操作
Apr 29 Golang
完美解决golang go get私有仓库的问题
May 05 Golang
基于Golang 高并发问题的解决方案
May 08 Golang
Golang实现AES对称加密的过程详解
May 20 Golang
Golang标准库syscall详解(什么是系统调用)
May 25 Golang
试了下Golang实现try catch的方法
Jul 01 Golang
golang中字符串MD5生成方式总结
Jul 04 Golang
Go语言基础切片的创建及初始化示例详解
Nov 17 Golang
Go语言特点及基本数据类型使用详解
Mar 21 Golang
Go web入门Go pongo2模板引擎
May 20 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
咖啡的化学
2021/03/03 咖啡文化
php session_start()关于Cannot send session cache limiter - headers already sent错误解决方法
2009/11/27 PHP
PHP编写学校网站上新生注册登陆程序的实例分享
2016/03/21 PHP
PHP二维数组实现去除重复项的方法【保留各个键值】
2017/12/21 PHP
PHP fprintf()函数用法讲解
2019/02/16 PHP
参考:关于Javascript中实现暂停的几篇文章
2007/03/04 Javascript
用javascript获取textarea中的光标位置
2008/05/06 Javascript
Javascript 文件夹选择框的两种解决方案
2009/07/01 Javascript
Javascript的构造函数和constructor属性
2010/01/09 Javascript
小试JQuery的AutoComplete插件
2011/05/04 Javascript
js两行代码按指定格式输出日期时间
2011/10/21 Javascript
Javascript 键盘事件的组合使用实现代码
2012/05/04 Javascript
node.js中的fs.chmod方法使用说明
2014/12/18 Javascript
js识别uc浏览器的代码
2015/11/06 Javascript
js判断当前页面用什么浏览器打开的方法
2016/01/06 Javascript
BootStrap实现鼠标悬停下拉列表功能
2017/02/17 Javascript
基于vue2实现左滑删除功能
2017/11/28 Javascript
通过fastclick源码分析彻底解决tap“点透”
2017/12/24 Javascript
Vue点击切换颜色的方法
2018/09/13 Javascript
JavaScript学习笔记之基于定时器实现图片无缝滚动功能详解
2019/01/09 Javascript
JS如何实现网站中PC端和手机端自动识别并跳转对应的代码
2020/01/08 Javascript
html2canvas属性和使用方法以及如何使用html2canvas将HTML内容写入Canvas生成图片
2020/01/12 Javascript
在antd Table中插入可编辑的单元格实例
2020/10/28 Javascript
详解Python中for循环的使用
2015/04/14 Python
简单介绍Python中的struct模块
2015/04/28 Python
Python实现感知器模型、两层神经网络
2017/12/19 Python
Python实现的从右到左字符串替换方法示例
2018/07/06 Python
使用Python操作FTP实现上传和下载的方法
2019/04/01 Python
Python 限定函数参数的类型及默认值方式
2019/12/24 Python
阿迪达斯俄罗斯官方商城:adidas俄罗斯
2017/03/08 全球购物
金蝶的一道SQL笔试题
2012/12/18 面试题
季度思想汇报
2014/01/01 职场文书
城管执法人员纪律作风整顿思想汇报
2014/09/13 职场文书
党支部四风整改方案
2014/10/25 职场文书
nginx配置之并发频次限制
2022/04/18 Servers
聊聊配置 Nginx 访问与错误日志的问题
2022/05/25 Servers