GO 中如何防止 goroutine 泄露
文章目录
- 概述
- 如何监控泄露
- 一个简单的例子
- 泄露情况分类
- chanel 引起的泄露
- 发送不接收
- 接收不发送
- nil channel
- 真实的场景
- 传统同步机制
- Mutex
- WaitGroup
- 总结
- 参考资料
今天来简单谈谈,Go 如何防止 goroutine 泄露。
概述
Go 的并发模型与其他语言不同,虽说它简化了并发程序的开发难度,但如果不了解使用方法,常常会遇到 goroutine 泄露的问题。虽然 goroutine 是轻量级的线程,占用资源很少,但如果一直得不到释放并且还在不断创建新协程,毫无疑问是有问题的,并且是要在程序运行几天,甚至更长的时间才能发现的问题。
对于上面描述的问题,我觉得可以从两方面入手解决,如下:
一是预防,要做到预防,我们就需要了解什么样的代码会产生泄露,以及了解如何写出正确的代码;
二是监控,虽说预防减少了泄露产生的概率,但没有人敢说自己不犯错,因而,通常我们还需要一些监控手段进一步保证程序的健壮性;
接下来,我将会分两篇文章分别从这两个角度进行介绍,今天先谈第一点。
如何监控泄露
本文主要集中在第一点上,但为了更好的演示效果,可以先介绍一个最简单的监控方式。通过 runtime.NumGoroutine() 获取当前运行中的 goroutine 数量,通过它确认是否发生泄漏。它的使用非常简单,就不为它专门写个例子了。
一个简单的例子
语言级别的并发支持是 Go 的一大优势,但这个优势也很容易被滥用。通常我们在开始 Go 并发学习时,常常听别人说,Go 的并发非常简单,在调用函数前加上 go 关键词便可启动 goroutine,即一个并发单元,但很多人可能只听到了这句话,然后就出现了类似下面的代码:
package mainimport ("fmt""runtime""time"
)func sayHello() {for {fmt.Println("Hello gorotine")time.Sleep(time.Second)}
}func main() {defer func() {fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()go sayHello()fmt.Println("Hello main")
}
对 Go 比较熟悉的话,很容易发现这段代码的问题,sayHello 是个死循环,没有如何退出机制,因此也就没有任何办法释放创建的 goroutine。我们通过在 main 函数最前面的 defer 实现在函数退出时打印当前运行中的 goroutine 数量,毫无意外,它的输出如下:
the number of goroutines: 2
不过,因为上面的程序并非常驻,有泄露问题也不大,程序退出后系统会自动回收运行时资源。但如果这段代码在常驻服务中执行,比如 http server,每接收到一个请求,便会启动一次 sayHello,时间流逝,每次启动的 goroutine 都得不到释放,你的服务将会离奔溃越来越近。
这个例子比较简单,我相信,对 Go 的并发稍微有点了解的朋友都不会犯这个错。
泄露情况分类
前面介绍的例子由于在 goroutine 运行死循环导致的泄露。接下来,我会按照并发的数据同步方式对泄露的各种情况进行分析。简单可归于两类,即:
- channel 导致的泄露
- 传统同步机制导致的泄露
传统同步机制主要指面向共享内存的同步机制,比如排它锁、共享锁等。这两种情况导致的泄露还是比较常见的。go 由于 defer 的存在,第二类情况,一般情况下还是比较容易避免的。
chanel 引起的泄露
先说 channel,如果之前读过官方的那篇并发的文章,翻译版,你会发现 channel 的使用,一个不小心就泄露了。我们来具体总结下那些情况下可能导致。
发送不接收
我们知道,发送者一般都会配有相应的接收者。理想情况下,我们希望接收者总能接收完所有发送的数据,这样就不会有任何问题。但现实是,一旦接收者发生异常退出,停止继续接收上游数据,发送者就会被阻塞。这个情况在 前面说的文章 中有非常细致的介绍。
示例代码:
package mainimport "time"func gen(nums ...int) <-chan int {out := make(chan int)go func() {for _, n := range nums {out <- n}close(out)}()return out
}func main() {defer func() {fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()// Set up the pipeline.out := gen(2, 3)for n := range out {fmt.Println(n) // 2time.Sleep(5 * time.Second) // done thing, 可能异常中断接收if true { // if err != nil break}}
}
例子中,发送者通过 out chan 向下游发送数据,main 函数接收数据,接收者通常会依据接收到的数据做一些具体的处理,这里用 Sleep 代替。如果这期间发生异常,导致处理中断,退出循环。gen 函数中启动的 goroutine 并不会退出。
如何解决?
此处的主要问题在于,当接收者停止工作,发送者并不知道,还在傻傻地向下游发送数据。故而,我们需要一种机制去通知发送者。我直接说答案吧,就不循渐进了。Go 可以通过 channel 的关闭向所有的接收者发送广播信息。
修改后的代码:
package mainimport "time"func gen(done chan struct{}, nums ...int) <-chan int {out := make(chan int)go func() {defer close(out)for _, n := range nums {select {case out <- n:case <-done:return}}}()return out
}func main() {defer func() {time.Sleep(time.Second)fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()// Set up the pipeline.done := make(chan struct{})defer close(done)out := gen(done, 2, 3)for n := range out {fmt.Println(n) // 2time.Sleep(5 * time.Second) // done thing, 可能异常中断接收if true { // if err != nil break}}
}
函数 gen 中通过 select 实现 2 个 channel 的同时处理。当异常发生时,将进入 <-done 分支,实现 goroutine 退出。这里为了演示效果,保证资源顺利释放,退出时等待了几秒保证释放完成。
执行后的输出如下:
the number of goroutines: 1
现在只有主 goroutine 存在。
接收不发送
发送不接收会导致发送者阻塞,反之,接收不发送也会导致接收者阻塞。直接看示例代码,如下:
package mainfunc main() {defer func() {time.Sleep(time.Second)fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()var ch chan struct{}go func() {ch <- struct{}{}}()
}
运行结果显示:
the number of goroutines: 2
当然,我们正常不会遇到这么傻的情况发生,现实工作中的案例更多可能是发送已完成,但是发送者并没有关闭 channel,接收者自然也无法知道发送完毕,阻塞因此就发生了。
解决方案是什么?那当然就是,发送完成后一定要记得关闭 channel。
nil channel
向 nil channel 发送和接收数据都将会导致阻塞。这种情况可能在我们定义 channel 时忘记初始化的时候发生。
示例代码:
func main() {defer func() {time.Sleep(time.Second)fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()var ch chan intgo func() {<-ch// ch<-}()
}
两种写法:<-ch 和 ch<- 1,分别表示接收与发送,都将会导致阻塞。如果想实现阻塞,通过 nil channel 和 done channel 结合实现阻止 main 函数的退出,这或许是可以一试的方法。
func main() {defer func() {time.Sleep(time.Second)fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()done := make(chan struct{})var ch chan intgo func() {defer close(done)}()select {case <-ch:case <-done:return}
}
在 goroutine 执行完成,检测到 done 关闭,main 函数退出。
真实的场景
真实的场景肯定不会像案例中的简单,可能涉及多阶段 goroutine 之间的协作,某个 goroutine 可能即使接收者又是发送者。但归根接底,无论什么使用模式。都是把基础知识组织在一起的合理运用。
传统同步机制
虽然,一般推荐 Go 并发数据的传递,但有些场景下,显然还是使用传统同步机制更合适。Go 中提供传统同步机制主要在 sync 和 atomic 两个包。接下来,我主要介绍的是锁和 WaitGroup 可能导致 goroutine 的泄露。
Mutex
和其他语言类似,Go 中存在两种锁,排它锁和共享锁,关于它们的使用就不作介绍了。我们以排它锁为例进行分析。
示例如下:
func main() {total := 0defer func() {time.Sleep(time.Second)fmt.Println("total: ", total)fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()var mutex sync.Mutexfor i := 0; i < 2; i++ {go func() {mutex.Lock()total += 1}()}
}
执行结果如下:
total: 1
the number of goroutines: 2
这段代码通过启动两个 goroutine 对 total 进行加法操作,为防止出现数据竞争,对计算部分做了加锁保护,但并没有及时的解锁,导致 i = 1 的 goroutine 一直阻塞等待 i = 0 的 goroutine 释放锁。可以看到,退出时有 2 个 goroutine 存在,出现了泄露,total 的值为 1。
怎么解决?因为 Go 有 defer 的存在,这个问题还是非常容易解决的,只要记得在 Lock 的时候,记住 defer Unlock 即可。
示例如下:
mutex.Lock()
defer mutext.Unlock()
其他的锁与这里其实都是类似的。
WaitGroup
WaitGroup 和锁有所差别,它类似 Linux 中的信号量,可以实现一组 goroutine 操作的等待。使用的时候,如果设置了错误的任务数,也可能会导致阻塞,导致泄露发生。
一个例子,我们在开发一个后端接口时需要访问多个数据表,由于数据间没有依赖关系,我们可以并发访问,示例如下:
package mainimport ("fmt""runtime""sync""time"
)func handle() {var wg sync.WaitGroupwg.Add(4)go func() {fmt.Println("访问表1")wg.Done()}()go func() {fmt.Println("访问表2")wg.Done()}()go func() {fmt.Println("访问表3")wg.Done()}()wg.Wait()
}func main() {defer func() {time.Sleep(time.Second)fmt.Println("the number of goroutines: ", runtime.NumGoroutine())}()go handle()time.Sleep(time.Second)
}
执行结果如下:
the number of goroutines: 2
出现了泄露。再看代码,它的开始部分定义了类型为 sync.WaitGroup 的变量 wg,设置并发任务数为 4,但是从例子中可以看出只有 3 个并发任务。故最后的 wg.Wait() 等待退出条件将永远无法满足,handle 将会一直阻塞。
怎么防止这类情况发生?
我个人的建议是,尽量不要一次设置全部任务数,即使数量非常明确的情况。因为在开始多个并发任务之间或许也可能出现被阻断的情况发生。最好是尽量在任务启动时通过 wg.Add(1) 的方式增加。
示例如下:
...wg.Add(1)go func() {fmt.Println("访问表1")wg.Done()}()wg.Add(1)go func() {fmt.Println("访问表2")wg.Done()}()wg.Add(1)go func() {fmt.Println("访问表3")wg.Done()}()...
总结
大概介绍完了我认为的所有可能导致 goroutine 泄露的情况。总结下来,其实无论是死循环、channel 阻塞、锁等待,只要是会造成阻塞的写法都可能产生泄露。因而,如何防止 goroutine 泄露就变成了如何防止发生阻塞。为进一步防止泄露,有些实现中会加入超时处理,主动释放处理时间太长的 goroutine。
本篇主要从如何写出正确代码的角度来介绍如何防止 goroutine 的泄露。下篇,将会介绍如何实现更好的监控检测,以帮助我们发现当前代码中已经存在的泄露。
参考资料
Concurrency In Go
Goroutine leak
Leaking-Goroutines
Go Concurrency Patterns: Context
Go Concurrency Patterns: Pipelines and cancellation
make goroutine stay running after returning from function
Never start a goroutine without knowing how it will stop
相关文章:

GO 中如何防止 goroutine 泄露
文章目录 概述如何监控泄露一个简单的例子泄露情况分类chanel 引起的泄露发送不接收接收不发送nil channel真实的场景 传统同步机制MutexWaitGroup 总结参考资料 今天来简单谈谈,Go 如何防止 goroutine 泄露。 概述 Go 的并发模型与其他语言不同,虽说它…...

Linux练习题
1 简答题:请列举你所知道的Linux发行版 常见的Linux发行版: Red Hat Enterprise Linux 6/7/8 CentOS 6/7/8 Suse Linux Enterprise 15 Debian Linux 11 Ubuntu Linux 20.04/21.04 Rocky Linux 8/9 2 简答题:Linux系统的根目录、/dev目录的作用是什么 /:linux文件系统的…...

storm统计服务开启zookeeper、kafka 、Storm(sasl认证)
部署storm统计服务开启zookeeper、kafka 、Storm(sasl认证) 当前测试验证结果: 单独配置zookeeper 支持acl 设置用户和密码,在storm不修改代码情况下和kafka支持当kafka 开启ACL时,storm 和ccod模块不清楚配置用户和密…...

YOLOv8加入AIFI模块,附带项目源码链接
YOLOv8" 是一个新一代的对象检测框架,属于YOLO(You Only Look Once)系列的最新版本。YOLOv8中提及的AIFI(Attention-based Intrascale Feature Interaction)模块是一种用于增强对象检测性能的机制,它是…...

【设计模式】代理模式的实现方式与使用场景
1. 概述 代理模式是一种结构型设计模式,它通过创建一个代理对象来控制对另一个对象的访问,代理对象在客户端和目标对象之间充当了中介的角色,客户端不再直接访问目标对象,而是通过代理对象间接访问目标对象。 那在中间加一层代理…...

医学图像的图像处理、分割、分类和定位-1
一、说明 本报告全面探讨了应用于医学图像的图像处理和分类技术。开展了四项不同的任务来展示这些方法的多功能性和有效性。任务 1 涉及读取、写入和显示 PNG、JPG 和 DICOM 图像。任务 2 涉及基于定向变化的多类图像分类。此外,我们在任务 3 中包括了胸部 X 光图像…...

【51单片机】外部中断
0、前言 参考:普中 51 单片机开发攻略 第16章 及17章 1、硬件 2、软件 #include <reg52.h> #include <intrins.h> #include "delayms.h"typedef unsigned char u8; typedef unsigned int u16;sbit led P2^0; sbit key3 P3^2;//外部中断…...

fastapi框架
fastapi框架 fastapi,一个用于构建 API 的现代、快速(高性能)的异步web框架。 fastapi是建立在Starlette和Pydantic基础上的 Pydantic是一个基于Python类型提示来定义数据验证、序列化和文档的库。Starlette是一种轻量级的ASGI框架/工具包…...

2023 年顶级前端工具
谁不喜欢一个好的前端工具?在本综述中,您将找到去年流行的有用的前端工具,它们将帮助您加快开发工作流程。让我们深入了解一下! 在过去的 12 个月里,我在我的时事通讯 Web Tools Weekly 中分享了数百种工具。我为前端…...

html 会跳舞的时间动画特效
下面是是代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns"http://www.w3.org/1999/xhtml"> <head> <meta h…...

微信AR实现识别手部展示glb模型
1.效果 2.微信小程序手势识别只支持以下几个动作,和识别点位,官方文档 因为AR识别手部一直在识别,所以会出现闪动问题。可以将微信开发者调试基础库设置到3.3.2以上,可能要稳定一些 3.3.代码展示,我用的是微信官方文…...

MYSQL自连接、子查询
自连接: # board表 mysql> select * from board; --------------------------------- | id | name | intro | parent_id | --------------------------------- | 1 | 后端 | NULL | NULL | | 2 | 前端 | NULL | NULL | | 3 | 移…...

docker搭建hbase 全部流程(包含本地API访问)
一、使用docker下载并安装hbase 1、搜索:docker search hbase 2、下载:docker pull harisekhon/hbase(一定要下载这个,下面都是围绕此展开的) 3、启动容器: docker run -d -p 2181:2181 -p 16000:16000…...

Mybatis之关联
一、一对多关联 eg:一个用户对应多个订单 建表语句 CREATE TABLE t_customer (customer_id INT NOT NULL AUTO_INCREMENT, customer_name CHAR(100), PRIMARY KEY (customer_id) ); CREATE TABLE t_order ( order_id INT NOT NULL AUTO_INCREMENT, order_name C…...

Labview实现用户界面切换的几种方式---通过VI间相互调用
在做用户界面时我们的程序往往面对的对象是程序使用者,复杂程序如果放在同一个页面中,往往会导致程序冗长卡顿,此时通过多个VI之间的切换就可以实现多个界面之间的转换,也会显得程序更加的高大上。 本文所有程序均可下载ÿ…...

点云从入门到精通技术详解100篇-基于点云和图像融合的智能驾驶目标检测(中)
目录 2.1.2 数据源选型分析 2.2 环境感知系统分析 2.2.1 传感器布置方案分析...

Apache-iotdb物联网数据库的安装及使用
一、简介 >Apache IoTDB (Database for Internet of Things) is an IoT native database with high performance for data management and analysis, deployable on the edge and the cloud. Due to its light-weight architecture, high performance and rich feature set…...

项目管理流程
优质博文 IT-BLOG-CN 一、简介 项目是为提供某项独特产品【独特指:创造出与以往不同或者多个方面与以往有所区别产品或服务,所以日复一日重复的工作就不属于项目】、服务或成果所做的临时性【临时性指:项目有明确的开始时间和明确的结束时间,不会无限期…...

0004.电脑开机提示按F1
常用的电脑主板不知道什么原因,莫名其妙的启动不了了。尝试了很多方法,没有奏效。没有办法我就只能把硬盘拆了下来,装到了另一台电脑上面。但是开机以后却提示F1,如下图: 根据上面的提示,应该是驱动有问题…...

中国电子学会2022年12月份青少年软件编程Scratch图形化等级考试试卷一级真题(含答案)
一、单选题(共25题,共50分) 1. 小明想在开始表演之前向大家问好并做自我介绍,应运行下列哪个程序?(2分) A. B. C. D. 2. 舞台有两个不同的背景,小猫角色的哪个积木能够切换舞台背景?(2分) A. B. C. D. 3. …...

C语言第二弹---C语言基本概念(下)
✨个人主页: 熬夜学编程的小林 💗系列专栏: 【C语言详解】 【数据结构详解】 C语言基本概念 1、字符串和\02、转义字符3、语句和语句分类3.1、空语句3.2、表达式语句3.3、函数调⽤语句3.4、复合语句3.5、控制语句 4、注释4.1、注释的两种形…...

Java 基础面试题 String(一)
Java 基础面试题 String(一) 文章目录 Java 基础面试题 String(一)String、StringBuffer、StringBuilder 的区别?String 为什么是不可变的?字符串拼接用“” 还是 StringBuilder? 文章来自Java Guide 用于学习如有侵…...

QT中QApplication对象有且只有一个
QT中QApplication对象有且只有一个 QApplication对象 QApplication对象 QApplication是应用程序对象 #include <QApplication> int main(int argc,char* argv[]); {//a对象在一个程序中有且只有一个,QT中要求必须有一个QApplication a(argc,argv…...

HTML CSS 发光字头特效
效果展示: 代码: <html><head> </head><style>*{margin: 0;padding: 0;}body {text-align: center;}h1{/* border: 3px solid rgb(201, 201, 201); */margin-bottom: 20px;}.hcqFont {position: relative;letter-spacing: 0.07…...

4.postman批量运行及json、cvs文件运行
一、批量运行collection 1.各个接口设置信息已保存,在collection中点击run collection 2.编辑并运行集合 集合运行时,单独上传图片时报错。需修改postman设置 二、csv文件运行 可新建记事本,输入测试数据,后另存为新的文本文件&…...

Superset二次开发之集成链路追踪TraceID技术
config.py ##时间-日志级别-完整路径-文件名字-文件行-函数名字-信息 LOG_FORMAT = "%(asctime)s:%(levelname)s:%(pathname)s:%(module)s:%(lineno)d:%(funcName)s:%(message)s" 字符串详细信息 格式字符串作用%(name)s日志记录器的名称(记录通道)%(levelno)s日…...

商品详情APP端原数据淘宝数据采集API接口代码接入示例
商品详情APP端原数据API接口(接口接入入口)的作用是提供APP端商品的详细信息,包括价格、描述、图片、折后价、优惠券信息等。通过调用这个API接口,开发者可以获取到APP端商品详情相关的数据,从而进行数据分析ÿ…...

企业官网搭建:打造专业形象的关键步骤
企业官网是企业在数字世界中的门面,搭建一个专业、功能齐全的官网对于企业的形象和业务发展至关重要。以下是一些关键的步骤: 一、确定目标和需求 明确网站的目标、受众和主要功能,为设计和内容提供指导。 二、域名和主机选择 选择易于记忆和…...

Vue2移动端项目使用$router.go(-1)不生效问题记录
目录 1、this.$router.go(-1) 改成 this.$router.back() 2、存储 from.path,使用 this.$router.push 3、hash模式中使用h5新增的onhashchange事件做hack处理 4、this.$router.go(-1) 之前添加一个 replace 方法 问题背景 : 在 Vue2 的一个移动端开发…...

ChatGPT与文心一言:AI助手之巅的对决
随着科技的飞速发展,人工智能助手已经渗透到我们的日常生活和工作中。 而在这个充满竞争的领域里,ChatGPT和文心一言无疑是最引人注目的两款产品。它们各自拥有独特的优势,但在智能回复、语言准确性、知识库丰富度等方面却存在差异。那么&am…...