原文地址

许多Go开发人员都熟悉这个格言: 在不知道如何停止的情况下,永远不要启动goroutine。然而,泄漏goroutines仍然非常容易。让我们看一下泄漏goroutine的一种常见方法以及如何修复它。

为此,我们将构建一个具有自定义map类型的库,map的键配置配置成在持续时间后过期。我们给这个库取名为ttl,它将具有如下所示的API:

// Create a map with a TTL of 5 minutes
m := ttl.NewMap(5*time.Minute)
// Set a key
m.Set("my-key", []byte("my-value"))// Read a key
v, ok := m.Get("my-key")
// "my-value"
fmt.Println(string(v))
// true, key is present
fmt.Println(ok)// ... more than 5 minutes later
v, ok := m.Get("my-key")
// no value here
fmt.Println(string(v) == "")
// false, key has expired
fmt.Println(ok)

为了确保键到期能被删除,我们在NewMap函数中启动一个工作协程:

func NewMap(expiration time.Duration) *Map {m := &Map{data:       make(map[string]expiringValue),expiration: expiration,}// start a worker goroutinego func() {for range time.Tick(expiration) {m.removeExpired()}}()return m
}

工作协程将根据配置时间,周期性的调用map类型的方法以删除任何过期的键。因此在调用SetKey时需要记录键添加的时间,这就是数据字段包含expiringValue类型的原因,该类型将实际值与到期时间相关联:

 type expiringValue struct {expiration time.Timedata       []byte // the actual value
}

乍一看,工作协程的调用似乎很好。如果这不是一个关于协程泄漏的帖子,这段代码也许不会有什么问题。但实际上,我们在构造函数中泄漏了一个协程。是怎么泄漏的呢?

让我们来看看Map的典型生命周期。首先,调用者创建Map的实例。创建实例后,一个工作协程开始运行。接下来,调用者可以对Set和Get进行任意数量的调用。但最终,调用者将完成使用Map实例并释放对它的所有引用。此时,垃圾收集器通常能够收集实例的内存。但是,工作仍在运行,并且还保留了Map实例的引用。由于没有明确的调用来停止工作协程,我们泄漏了一个协程,并泄漏了Map实例的内存。

我们把问题再搞得明显一点。为达到目的,我们将使用运行时包来查看有关内存分配器的统计信息以及在特定时刻运行的Go协程的数量。

func main() {go func() {var stats runtime.MemStatsfor {runtime.ReadMemStats(&stats)fmt.Printf("HeapAlloc    = %d\n", stats.HeapAlloc)fmt.Printf("NumGoroutine = %d\n", runtime.NumGoroutine())time.Sleep(5*time.Second)}}()for {work()}
}func work() {m := ttl.NewMap(5*time.Minute)m.Set("my-key", []byte("my-value"))if _, ok := m.Get("my-key"); !ok {panic("no value present")}// m goes out of scope
}

很快就可以看到堆分配和Go协程的数量增长很多。

HeapAlloc    = 76960
NumGoroutine = 18
HeapAlloc    = 2014278208
NumGoroutine = 1447847
HeapAlloc    = 3932578560
NumGoroutine = 2832416
HeapAlloc    = 5926163224
NumGoroutine = 4322524

所以现在很明显我们需要停止那个Go协程。目前,Map API没有办法关闭工作协程。最好避免修改任何API,并在调用者使用完Map实例时能停止工作协程。但只有调用者知道什么时候该停止。

解决此问题的常见模式是实现io.Closer接口。当调用者使用完Map时,他们可以调用Close来告诉Map停止其工作协程。

func (m *Map) Close() error {close(m.done)return nil
}

现在在我们的构造函数中需要像下面这样启动工作协程:

func NewMap(expiration time.Duration) *Map {m := &Map{data:       make(map[string]expiringValue),expiration: expiration,done:       make(chan struct{}),}// start a worker goroutinego func() {ticker := time.NewTicker(expiration)defer ticker.Stop()for {select {case <-ticker.C:m.removeExpired()case <-m.done:return}}}()return m
}

现在,工作协程包含一个select语句,除了计时器的Channel之外还会检查done Channel。注意,我们也更改了timer.TIcker的调用。请注意,因为之前的版本没有调用ticker.Stop,也会泄漏。

修改之后,我们简单的分析看起来像这样:

HeapAlloc    = 72464
NumGoroutine = 6
HeapAlloc    = 5175200
NumGoroutine = 59
HeapAlloc    = 5495008
NumGoroutine = 35
HeapAlloc    = 9171136
NumGoroutine = 240
HeapAlloc    = 8347120
NumGoroutine = 53

数字很​​小,这是因为调用工作协程的循环比较小。但更重要的是,Go协程或堆分配数量没有再大幅增长了。这正是我们想要的结果。注意,在这里可以找到完整的代码。

这篇文章展示了一个明显的例子,说明为什么知道何时停止Go协程如此重要。另一方面,也可以说监控应用程序中的Go协程数量一样很重要。如果Go协程泄漏潜入代码库,这样的监控程序可以提供一个警告系统。还值得记住的是,有时Go协程泄漏需要数天甚至数周才能在应用程序中显示,所以拥有更短和更长时间跨度的监视程序很有必要。

原文地址:How to Leak a Goroutine and Then Fix It

Go协程泄漏和修复方法相关推荐

  1. 分析Kotlin协程只挂起不恢复会怎样(是否存在协程泄漏),以及挂起的协程存在哪里?

    前言 刚开始正式学协程原理的时候(以前只是学api怎么用),大概是20年6月,也就是bennyhuo大佬出书<深入理解Kotlin协程>的时候,我买了本然后细细研究,我的内心就一直有一个问 ...

  2. Kotlin实战指南十五:协程泄漏

    转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/106413283 本文出自[赵彦军的博客] 文章目录 协程泄漏的本质 Global ...

  3. python协程三种实现方法

    协程的三种方法 1,协程中名词 event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上.当满足事件发生的时候,调用相应的协程函数. coroutine 协程 ...

  4. 协程泄漏之gops使用解析

    1.协程泄漏问题 如果你启动了一个goroutine,但并没有按照预期的一样退出,直到程序结束,此goroutine才结束,这种情况就是 goroutine 泄露.当 goroutine 泄露发生时, ...

  5. Kotlin协程使用,协程使用注意事项,协程中的await方法使用|不使用suspend使用协程

    参见 码云 协程使用方法一 (Dispatchers调度器模式) 指定不同线程.同线程会挂起并阻塞(挂起是不影响主线程执行,阻塞是同样的IO线程会阻塞) withContext(Dispatchers ...

  6. Unity中协程(IEnumerator)的使用方法介绍

    在Unity中,一般的方法都是顺序执行的,一般的方法也都是在一帧中执行完毕的,当我们所写的方法需要耗费一定时间时,便会出现帧率下降,画面卡顿的现象.当我们调用一个方法想要让一个物体缓慢消失时,除了在U ...

  7. Python协程中生成器send方法的使用

    关于博主 努力与运动兼备-~~有任何问题可以加我好友或者关注微信公众号,欢迎交流,我们一起进步! 微信公众号: 啃饼思录 QQ: 2810706745(i思录) 写在前面 博主最近在利用协程写异步爬虫 ...

  8. Python 中 异步协程 的 使用方法介绍

    静觅 崔庆才的个人博客:Python中异步协程的使用方法介绍:https://cuiqingcai.com/6160.html Python 异步 IO .协程.asyncio.async/await ...

  9. Golang 协程的使用方法

    Golang 协程正确的使用方法 错误的使用方法 package main // 错误使用案例 import ("time""fmt" ) var c1 cha ...

最新文章

  1. 何时该用无服务器,何时该用Kubernetes?
  2. ECharts概念学习系列之ECharts是什么?
  3. <马哲>商品二因素及其辩证关系2017-12-27
  4. 20应用统计考研复试要点(part41)--概率论与数理统计
  5. 中山大学校队选拔赛第二试题试题3【Compressed suffix array】-------2015年2月8日
  6. 将markdown格式转化为bootstrap风格html
  7. excel流程图分叉 合并_excel流程图怎么画
  8. 基于Tiny6410的LCD与一线触屏移植
  9. 大数据在各领域应用之精准营销
  10. 路由器基本设置(一)
  11. 【数据结构】- 几个步骤教你认识并实现一个链表之带头(哨兵位)双向循环链表(中)
  12. 为什么需要制定计划?
  13. vdbench 配置案例及参数说明
  14. Android 按两次返回键、长按返回键退出程序
  15. leetcode 1074. Number of Submatrices That Sum to Target(和为target的子矩阵个数)
  16. 用卡尔曼滤波处理工程数据的方法与思考with基于GPS与INS组合导航的滤波模型仿真
  17. L11.linux命令每日一练 -- 第二章 文件和目录操作命令 -- rename和basename命令
  18. 定位追踪器百科:宠物、老人、小孩、汽车通用的定位器
  19. SO(3)的不可约表示
  20. pygame,上下左右移动

热门文章

  1. PS快速祛除脸上小雀斑
  2. 【后端】Python体系(一)
  3. lighting 光照简介(个人笔记)
  4. 【树莓派不吃灰】基础篇⑪ 开机自动挂载闲置U盘
  5. mac反向控制iphone_反向工程iPhone X Home指示灯颜色
  6. 命令神器-thefuck
  7. 观Illidan被FD有感
  8. KNN最近邻算法分析及实现(Python实现)
  9. DMP-BDT110 - 短概览松下的新的3D蓝光光盘播放机
  10. 多台电脑共用鼠标键盘(局域网内)