Go语言死锁与goroutine泄露问题的解决

目录
  • 什么时候会导致死锁
  • 发送单个值时的死锁
  • 多个值发送的死锁
  • 解决多值发送死锁
  • 应该先发送还是先接收
  • goroutine 泄漏
  • 如何发现泄露
  • 小结

什么时候会导致死锁

在计算机组成原理里说过 死锁有三个必要条件他们分别是 循环等待、资源共享、非抢占式,在并发中出现通道死锁只有两种情况:

  • 数据要发送,但是没有人接收
  • 数据要接收,但是没有人发送

发送单个值时的死锁

牢记这两点问题就很清晰了,复习下之前的例子,会死锁

a := make(chan int)
a <- 1   //将数据写入channel
z := <-a //从channel中读取数据
  • 有且只有一个协程时,无缓冲的通道
  • 先发送会阻塞在发送,先接收会阻塞在接收处。
  • 发送操作在接收者准备好之前是阻塞的,接收操作在发送之前是阻塞的,

解决办法就是改为缓冲通道,或者使用协程配对

解决方法一,协程配对,先发送还是先接收无所谓只要配对就好

chanInt := make(chan int)
go func() {
    chanInt <- 1
}()

res := <-chanInt

解决方法二,缓冲通道

chanInt := make(chan int,1)
chanInt <- 2
res := <-chanInt
  • 缓冲通道内部的消息数量用len()函数可以测试出来
  • 缓冲通道的容量可以用cap()测试出来
  • 在满足cap>len时候,因为没有满,发送不会阻塞
  • 在len>0时,因为不为空,所以接收不会阻塞

使用缓冲通道可以让生产者和消费者减少阻塞的可能性,对异步操作更友好,不用等待对方准备,但是容量不应设置过大,不然会占用较多内存。

多个值发送的死锁

配对可以让死锁消失,但发送多个值的时候又无法配对了,又会死锁

func multipleDeathLock() {
 chanInt := make(chan int)
 defer close(chanInt)
    go func() {
  res := <-chanInt
  fmt.Println(res)
 }()
 chanInt <- 1
 chanInt <- 1
}

不出所料死锁了

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.multipleDeathLock()

在工作中只有通知信号是一对一的情况,通知一次以后就不再使用了,其他这种要求多次读写配对的情况根本不会存在。

解决多值发送死锁

更常见的是用循环来不断接收值,接受一个处理一个,如下:

func multipleLoop() {
 chanInt := make(chan int)
 defer close(chanInt)
 go func() {
  for {
   //不使用ok会goroutine泄漏
   //res := <-chanInt
   res,ok := <-chanInt
   if !ok {
                 break
            }
   fmt.Println(res)
  }
 }()
 chanInt <- 1
 chanInt <- 1
}

输出:

1
1

  • 给通道的接收加上二值,ok 代表通道是否正常,如果是关闭则为false值
  • 可以删掉那段逻辑试试,会输出1 2 0 0 0这样的数列,因为关闭是需要时间的,而循环接收关闭的通道拿到的是0
  • 关于goroutine泄漏稍后会讲到

应该先发送还是先接收

假如我们调换一下位置,把接收放外面,写入放里面会发生什么

func multipleDeathLock2() {
 chanInt := make(chan int)
 defer close(chanInt)
 go func() {
  chanInt <- 1
  chanInt <- 2
 }()
 for {
  res, ok := <-chanInt
  if !ok {
   break
  }
  fmt.Println(res)
 }
}

输出死锁
1
2
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.multipleDeathLock2()

  • 出现上面的结果是因为for循环一直在获取通道中的值,但是在读取完1 2后,通道中没有新的值传入,这样接收者就阻塞了。
  • 为什么先接收再发送可以,因为发送提前结束后会触发函数的defer自动关闭通道
  • 所以我们应该总是先接收后发送,并由发送端来关闭

goroutine 泄漏

goroutine 终止的场景有三个:

  • 当一个 goroutine 完成了它的工作
  • 由于发生了没有处理的错误
  • 有其他的协程告诉它终止

当三个条件都没有满足,goroutine 就会一直运行下去

func goroutineLeak() {
 chanInt := make(chan int)
 defer close(chanInt)
 go func() {
  for {
            res := <-chanInt
   //res,ok := <-chanInt
   //if !ok {
            //     break
            //}
   fmt.Println(res)
  }
 }()
 chanInt <- 1
 chanInt <- 1
}

上面的goroutineLeak()函数结束后触发defer close(chanInt)关闭了通道
但是匿名函数中goroutine并没有关闭,而是一直在循环取值,并且取到是的关闭后的通道值(这里是int的默认值 0)
goroutine会永远运行下去,如果以后再次使用又会出现新的泄漏!导致内存、cpu占用越来越多

输出,如果程序不停止就会一直输出0
1
1
0
0
0
...

假如不关闭且外部没有写入值,那接收处就会永远阻塞在那里,连输出都不会有

func goroutineLeakNoClosed() {
 chanInt := make(chan int)
 go func() {
  for {
            res := <-chanInt
   fmt.Println(res)
  }
 }()
}
  • 无任何输出的阻塞
  • 换成写入也是一样的
  • 如果是有缓冲的通道,换成已满的通道写没有读;或者换成向空的通道读没有写也是同样的情况
  • 除了阻塞,goroutine进入死循环也是泄露的原因

如何发现泄露

  • 使用 golang 自带的pprof监控工具,可以发现内存上涨情况,这个后续会讲
  • 还可以监控进程的内存使用情况,比如prometheus提供的process-exporter
  • 如果你有内存泄露/goroutine 泄露代码扫描的工具,欢迎留言,感恩!

小结

今天我们学习了一些细节,但是相当重要的知识点,也是未来面试高频问题哦!

  • 如果是信号通知,应该保证一一对应,不然会死锁
  • 除了信号通知外,通常我们使用循环处理通道,在工作中不断的处理数据
  • 应该总是先接收后发送,并由发送端来关闭,不然容易死锁或者泄露
  • 在接收处,应该对通道是否关闭做好判断,已关闭应该退出接收,不然会泄露
  • 小心 goroutine 泄漏,应该在通道关闭的时候及时检查通道并退出
  • 除了阻塞,goroutine进入死循环也是泄露的原因

本节源码地址

到此这篇关于Go语言死锁与goroutine泄露问题谈论的文章就介绍到这了,更多相关Go语言死锁与goroutine泄露内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Go 防止 goroutine 泄露的方法

    概述 Go 的并发模型与其他语言不同,虽说它简化了并发程序的开发难度,但如果不了解使用方法,常常会遇到 goroutine 泄露的问题.虽然 goroutine 是轻量级的线程,占用资源很少,但如果一直得不到释放并且还在不断创建新协程,毫无疑问是有问题的,并且是要在程序运行几天,甚至更长的时间才能发现的问题. 对于上面描述的问题,我觉得可以从两方面入手解决,如下: 一是预防,要做到预防,我们就需要了解什么样的代码会产生泄露,以及了解如何写出正确的代码: 二是监控,虽说预防减少了泄露产生的概率,但

  • Go语言死锁与goroutine泄露问题的解决

    目录 什么时候会导致死锁 发送单个值时的死锁 多个值发送的死锁 解决多值发送死锁 应该先发送还是先接收 goroutine 泄漏 如何发现泄露 小结 什么时候会导致死锁 在计算机组成原理里说过 死锁有三个必要条件他们分别是 循环等待.资源共享.非抢占式,在并发中出现通道死锁只有两种情况: 数据要发送,但是没有人接收 数据要接收,但是没有人发送 发送单个值时的死锁 牢记这两点问题就很清晰了,复习下之前的例子,会死锁 a := make(chan int) a <- 1 //将数据写入channel

  • Go语言轻量级线程Goroutine用法实例

    本文实例讲述了Go语言轻量级线程Goroutine用法.分享给大家供大家参考.具体如下: goroutine 是由 Go 运行时环境管理的轻量级线程. go f(x, y, z) 开启一个新的 goroutine 执行 f(x, y, z) f,x,y 和 z 是当前 goroutine 中定义的,但是在新的 goroutine 中运行 f. goroutine 在相同的地址空间中运行,因此访问共享内存必须进行同步. sync 提供了这种可能,不过在 Go 中并不经常用到,因为有其他的办法.(以

  • Java语言中的内存泄露代码详解

    Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同. JAVA中的内存管理 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的. 在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上. 下面看一个示例: public class Simple { public static vo

  • Go语言学习之goroutine详解

    什么是goroutine? Goroutine是建立在线程之上的轻量级的抽象.它允许我们以非常低的代价在同一个地址空间中并行地执行多个函数或者方法.相比于线程,它的创建和销毁的代价要小很多,并且它的调度是独立于线程的.在golang中创建一个goroutine非常简单,使用"go"关键字即可: package mainimport ( "fmt" "time")func learning() { fmt.Println("My firs

  • Golang 语言控制并发 Goroutine的方法

    goroutine 是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理.Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU. 01介绍 Golang 语言的优势之一是天生支持并发,我们在 Golang 语言开发中,通常使用的并发控制方式主要有 Channel,WaitGroup 和 Context,本文我们主要介绍一下 Golang 语言中并发控制的这三种方式怎么使用?关于它们各自的详细介绍在之前的文章已经介绍过,感兴趣的读者朋友们可以按需翻阅. 02

  • C语言数组元素循环右移问题及解决方法

    C 语言是一种通用的.面向过程式的计算机程序设计语言.1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言. C 语言是一种广泛使用的计算机语言,它与 Java 编程语言一样普及,二者在现代软件程序员之间都得到广泛使用. 题目内容: 解题思路 方法一: 这是我自己摸索出来的一种方法,通过对数组的多次逆置来达到循环的效果,一共对数组进行了三次逆置,所以就需要构造一个函数, 这个函数既可以逆置一个数组,也可以逆置数组的部分区间. 1,先将整个数组[0,N

  • Java中的内存泄露问题和解决办法

    目录 为什么会产生内存泄漏? 内存泄漏对程序的影响? 如何检查和分析内存泄漏? 常见的内存泄漏及解决方法 1.单例造成的内存泄漏 2.非静态内部类创建静态实例造成的内存泄漏[已无] 3.Handler造成的内存泄漏 4.线程造成的内存泄漏 5.资源未关闭造成的内存泄漏 6.使用ListView时造成的内存泄漏 7.集合容器中的内存泄露 8.WebView造成的泄露 如何避免内存泄漏? 总结 (Memory Leak,内存泄漏) 为什么会产生内存泄漏? 当一个对象已经不需要再使用本该被回收时,另外

  • C语言中的内存泄露 怎样避免与检测

    有些程序并不需要管理它们的动态内存的使用.当需要内存时,它们简单地通过分配来获得,从来不用担心如何释放它.这类程序包括编译器和其他一些运行一段固定的(或有限的)时间然后终止的程序.当这种类型的程序终止时,所有内存会被自动回收.细心查验每块内存是否需要回收纯属浪费时间,因为它们不会再被使用. 其他程序的生存时间要长一点.有些工具如日历管理器.邮件工具以及操作系统本事经常需要数日及至数周连续运行,并需要管理动态内存的分配和回收.由于C语言通常并不使用垃圾回收器(自动确认并回收不再使用的内存块),这些

  • nodejs和C语言插入mysql数据库乱码问题的解决方法

    在这里记录了nodejs过程中的一些乱码情况,这里的乱码主要是由于网页的编码方式与nodejs的默认解码方式(utf8)不一致所造成的.这一次要说一下的是在C语言和nodejs与MySQL进行交互的时候出现的乱码问题. 1,由于爬虫程序在多个Docker中执行,因此我需要定期的同步每一个docker中的mysql数据到一个全局的mysql数据表中.使用nodejs进行数据同步,出现中文乱码.要知道在每一docker中的中文是不存在乱码的.原因是nodejs默认处理字符是utf8,而mysql默认

随机推荐