面试高频:Go语言死锁与goroutine泄露问题谈论

简介: 面试高频:Go语言死锁与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进入死循环也是泄露的原因

相关文章
|
23小时前
|
缓存 Go 调度
浅谈在go语言中的锁
【5月更文挑战第11天】本文评估了Go标准库`sync`中的`Mutex`和`RWMutex`性能。`Mutex`包含状态`state`和信号量`sema`,不应复制已使用的实例。`Mutex`适用于保护数据,而`RWMutex`在高并发读取场景下更优。测试显示,小并发时`Mutex`性能较好,但随着并发增加,其性能下降;`RWMutex`的读性能稳定,写性能在高并发时低于`Mutex`。
115 0
浅谈在go语言中的锁
|
1天前
|
存储 安全 编译器
go语言中进行不安全的类型操作
【5月更文挑战第10天】Go语言中的`unsafe`包提供了一种不安全但强大的方式来处理类型转换和底层内存操作。包含两个文档用途的类型和八个函数,本文也比较了不同变量和结构体的大小与对齐系数,强调了字段顺序对内存分配的影响。
48 8
go语言中进行不安全的类型操作
|
1天前
|
Go
配置go语言下载包 - 蓝易云
这个命令会将包下载到你的GOPATH目录下,并自动安装它。
26 1
|
2天前
|
安全 Go 调度
Go语言中的并发编程
Go语言自带了强大的并发编程能力,它的协程机制可以让程序轻松地实现高并发。本文将从并发编程的基础概念出发,介绍Go语言中的协程机制、通道和锁等相关知识点,帮助读者更好地理解并发编程在Go语言中的实践应用。
|
4天前
|
Ubuntu Unix Linux
【GO基础】1. Go语言环境搭建
【GO基础】1. Go语言环境搭建
|
5天前
|
JSON 前端开发 Go
lucky - go 语言实现的快速开发平台
go 语言实现的快速开发平台,自动生成crud代码,前端页面通过json配置,无需编写前端代码。
11 0
|
6天前
|
存储 Java Go
Go 语言切片如何扩容?(全面解析原理和过程)
Go 语言切片如何扩容?(全面解析原理和过程)
16 2
|
6天前
|
负载均衡 Go 调度
使用Go语言构建高性能的Web服务器:协程与Channel的深度解析
在追求高性能Web服务的今天,Go语言以其强大的并发性能和简洁的语法赢得了开发者的青睐。本文将深入探讨Go语言在构建高性能Web服务器方面的应用,特别是协程(goroutine)和通道(channel)这两个核心概念。我们将通过示例代码,展示如何利用协程处理并发请求,并通过通道实现协程间的通信和同步,从而构建出高效、稳定的Web服务器。
|
6天前
|
算法 Go 分布式数据库
构建高可用的分布式数据库集群:使用Go语言与Raft共识算法
随着数据量的爆炸式增长,单一数据库服务器已难以满足高可用性和可扩展性的需求。在本文中,我们将探讨如何使用Go语言结合Raft共识算法来构建一个高可用的分布式数据库集群。我们不仅会介绍Raft算法的基本原理,还会详细阐述如何利用Go语言的并发特性和网络编程能力来实现这一目标。此外,我们还将分析构建过程中可能遇到的挑战和解决方案,为读者提供一个完整的实践指南。
|
6天前
|
消息中间件 Go API
基于Go语言的微服务架构实践
随着云计算和容器化技术的兴起,微服务架构成为了现代软件开发的主流趋势。Go语言,以其高效的性能、简洁的语法和强大的并发处理能力,成为了构建微服务应用的理想选择。本文将探讨基于Go语言的微服务架构实践,包括微服务的设计原则、服务间的通信机制、以及Go语言在微服务架构中的优势和应用案例。