经典面试题--golang交替打印cat、dog 以及出现死锁问题的分析

2023-10-13 19:30

本文主要是介绍经典面试题--golang交替打印cat、dog 以及出现死锁问题的分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

问题背景

一道经典面试题,如下:

  1.  使用两个goroutine循环打印,cat,dog
  2. 各打印5次

熟悉golang的同学,很容易写出如下代码:

func Pprint() {wg := sync.WaitGroup{}wg.Add(2)ch1, ch2 := make(chan struct{}), make(chan struct{})go func() {cnt := 0for {<-ch1fmt.Println("cat")cnt++ch2 <- struct{}{}if cnt == 5 {wg.Done()return}}}()go func() {cnt := 0for {<-ch2fmt.Println("do")cnt++ch1 <- struct{}{}if cnt == 5 {wg.Done()return}}}()ch1 <- struct{}{}wg.Wait()
}

这段代码,逻辑思路上没任何问题:

  1. 构建两个channel当作通信的信号量,当某个信号量操作完成后通知另一个完成;
  2. 通过sync.WaitGroup控制goroutine优雅退出。

问题

然而不幸的是,这段代码最终会出现死锁 panic:

 

 正确打印五次后,却出现了死锁的panic,没有实现程序优雅退出。

那么原因是什么呢?

我们来分析死锁检测时报错堆栈信息,其中有一个关键信息引起了我们的注意

 

 注意看第一行,上面报错信息的意思是:在func2协程(goroutine id是6),也就是我们代码中的第二个goroutine出现了因为 chan send阻塞导致的死锁

到这里肯定就有同学困惑了,我都是刚好5次呀,而且两个channel  ch1、ch2都是完全同步的。怎么就会因为chan send造成死锁呢???

继续看堆栈报错信息,还有一个关键的点,如下:

看到这里,更懵逼了吧。为什么第二个goroutine因为chan send和waitgroup产生了死锁???

我们不妨大胆猜测一下,是不是因为第二个gorouine因为某些原因无法正常退出,导致其wg.Done方法没法执行,因此系统检测到wg.Wait()所在的主协程永远无法执行完成,所以报出了deadlock panic。

下面我们在协程退出的地方加一些debug 日志来验证我们的猜想(重复代码省略了):

	go func() {cnt := 0for {...省略...if cnt == 5 {wg.Done()fmt.Println("exit1")return}}}()go func() {cnt := 0for {...省略...if cnt == 5 {wg.Done()fmt.Println("exit2")return}}}()

 debug结果如下:

果然不出我们的猜测,只有第一个goroutine正常退出了,第二个goroutine都来不及退出,就被死锁检测到。

到这里我们也能大致知道原因了:第一个goroutine ch1正常退出之后,在第二个goroutine里,系统判断出 ch1 <- struct{}{} 这一句代码永远不会有接收者;又因为ch1是阻塞的channel,所以系统会认为这里会死锁,就抛出deadlock panic。

其实问题的关键点在于:ch1和ch2都是阻塞的channel

解决方案

最后,解决方案也很明了:

1. 方案一:使用带缓冲的channel

ch1, ch2 := make(chan struct{}, 1), make(chan struct{}, 1)

2. 方案二:使用select防止阻塞

总结

这个交替打印的问题本身并不复杂,但是却非常经典。考验我们对channel的理解,对并发编程技巧的掌握,以及控制协程优雅退出的能力。

日常coding中,需要清楚的了解业务的场景,然后考虑是否使用带缓冲的channel

这篇关于经典面试题--golang交替打印cat、dog 以及出现死锁问题的分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/205369

相关文章

解读为什么@Autowired在属性上被警告,在setter方法上不被警告问题

《解读为什么@Autowired在属性上被警告,在setter方法上不被警告问题》在Spring开发中,@Autowired注解常用于实现依赖注入,它可以应用于类的属性、构造器或setter方法上,然... 目录1. 为什么 @Autowired 在属性上被警告?1.1 隐式依赖注入1.2 IDE 的警告:

解决java.lang.NullPointerException问题(空指针异常)

《解决java.lang.NullPointerException问题(空指针异常)》本文详细介绍了Java中的NullPointerException异常及其常见原因,包括对象引用为null、数组元... 目录Java.lang.NullPointerException(空指针异常)NullPointer

Android开发中gradle下载缓慢的问题级解决方法

《Android开发中gradle下载缓慢的问题级解决方法》本文介绍了解决Android开发中Gradle下载缓慢问题的几种方法,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、网络环境优化二、Gradle版本与配置优化三、其他优化措施针对android开发中Gradle下载缓慢的问

关于Nginx跨域问题及解决方案(CORS)

《关于Nginx跨域问题及解决方案(CORS)》文章主要介绍了跨域资源共享(CORS)机制及其在现代Web开发中的重要性,通过Nginx,可以简单地解决跨域问题,适合新手学习和应用,文章详细讲解了CO... 目录一、概述二、什么是 CORS?三、常见的跨域场景四、Nginx 如何解决 CORS 问题?五、基

MySQL安装时initializing database失败的问题解决

《MySQL安装时initializingdatabase失败的问题解决》本文主要介绍了MySQL安装时initializingdatabase失败的问题解决,文中通过图文介绍的非常详细,对大家的学... 目录问题页面:解决方法:问题页面:解决方法:1.勾选红框中的选项:2.将下图红框中全部改为英

golang字符串匹配算法解读

《golang字符串匹配算法解读》文章介绍了字符串匹配算法的原理,特别是Knuth-Morris-Pratt(KMP)算法,该算法通过构建模式串的前缀表来减少匹配时的不必要的字符比较,从而提高效率,在... 目录简介KMP实现代码总结简介字符串匹配算法主要用于在一个较长的文本串中查找一个较短的字符串(称为

Nginx启动失败:端口80被占用问题的解决方案

《Nginx启动失败:端口80被占用问题的解决方案》在Linux服务器上部署Nginx时,可能会遇到Nginx启动失败的情况,尤其是错误提示bind()to0.0.0.0:80failed,这种问题通... 目录引言问题描述问题分析解决方案1. 检查占用端口 80 的进程使用 netstat 命令使用 ss

mybatis和mybatis-plus设置值为null不起作用问题及解决

《mybatis和mybatis-plus设置值为null不起作用问题及解决》Mybatis-Plus的FieldStrategy主要用于控制新增、更新和查询时对空值的处理策略,通过配置不同的策略类型... 目录MyBATis-plusFieldStrategy作用FieldStrategy类型每种策略的作

linux下多个硬盘划分到同一挂载点问题

《linux下多个硬盘划分到同一挂载点问题》在Linux系统中,将多个硬盘划分到同一挂载点需要通过逻辑卷管理(LVM)来实现,首先,需要将物理存储设备(如硬盘分区)创建为物理卷,然后,将这些物理卷组成... 目录linux下多个硬盘划分到同一挂载点需要明确的几个概念硬盘插上默认的是非lvm总结Linux下多

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录