很久没写博客了,不得不说go语言爱好者周刊是个宝贝,本来想随便看看打发时间的,没想到一下子给了我久违的灵感。

go语言爱好者周刊78期出了一道非常有意思的题目。

我们来看看题目。先给出如下的代码:

package main

import (
"fmt"
"time"
) func main() {
ch1 := make(chan int)
go fmt.Println(<-ch1)
ch1 <- 5
time.Sleep(1 * time.Second)
}

请问这串代码的输出是什么。

我最先想到的是5,毕竟代码很简单,反应比较快的话代码看完结果也就推断出来了。

然而题目给出的其中一个选项是输出死锁报错,这个选项引起了我的好奇,于是我运行了一下:

$ go run a.go

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
/tmp/a.go:10 +0x65
exit status 2

啊这。真的死锁了。那么我猜会不会和执行顺序有关呢?于是我写了个脚本运行1000次看看:

#!/bin/bash

for i in {0..1000}
do
go run a.go &> /dev/null
if [ $? -eq 0 ]
then
echo 'success!'
break
fi
done

结果自然是一次也没成功,即使你改成10000哪怕是1000000也是一样的。执行顺序带来的影响我们可以排除了。

如果你仔细观察的话,所有的报错也都是一样的:goroutine 1 [chan receive]:,在这里死锁了。

那么会不会是因为使用了无缓冲chan的原因呢?golang的内存模型规定了无缓冲chan的接受happens before发送操作,这会不会带来影响呢(其实仔细想想就很快排除了,happens before确定的是内存的可见性,而不是指令执行的时间顺序),所以我改了下代码:

func main() {
ch1 := make(chan int, 100)
go fmt.Println(<-ch1)
ch1 <- 5
time.Sleep(1 * time.Second)
}

这次我们使用了一个有容纳100个元素的buff的channel,然而结果还是没有一点改变。

到这里我的思路中断了。

不过我还有google啊,所以我用“golang channel deadlock”为关键词搜索了一下,然后发现了一些有意思的结果。

那就是所有的chan的死锁的代码基本都能抽象成下面的形式:

func main() {
ch1 := make(chan int) // 是否有buff无影响
_ = <-chan
ch1 <- 5
}

这个代码毫无疑问是会死锁的,因为从chan接收值而chan里是空的会导致当前goroutine进入等待,而当前goroutine不能继续运行的话就永远没办法向chan里写入值,死锁就在这里产生了。

在仔细观察一下,你就会发现题目的代码和这很像:

func main() {
ch1 := make(chan int)
go fmt.Println(<-ch1)
ch1 <- 5
// sleep是为了main routine不会过早退出
}

答案只有一个,<-ch1发生在main goroutine里了。

为了佐证这一观点,我有查阅了golang language spec,关于go语句有如下的描述:

The function value and parameters are evaluated as usual in the calling goroutine, but unlike with a regular call, program execution does not wait for the invoked function to complete.

函数和它的参数会像通常那样在使用go语句的那个goroutine里被执行,但不像常规的函数调用,程序不会同步等待这个函数执行完毕。

如果在看看有关求值的部分:

calls f with arguments a1, a2, … an. Except for one special case, arguments must be single-valued expressions assignable to the parameter types of F and are evaluated before the function is called.

用参数a1, a2等调用函数f,出了一个特例之外他们都必须是单值表达式,并且在函数运行前被求值。

上面说的特例是方法调用,方法的receiver会用特定的位置传给method。

这样事情的来龙去脉就清晰明了了,我们来梳理一下。

假设我们在main goroutine里启动一个子goroutine叫b,那么实际上在main goroutine里发生的事情是这样的:

  1. main goroutine执行到go语句
  2. go语句发现后面的函数表达式需要传递参数
  3. 于是被传递的参数在main goroutine里求值
  4. 新的goroutine b被创建,刚求值的参数传递给需要执行的函数(假设叫f),f在goroutine b中开始执行
  5. go语句结束,控制流程回到main goroutine

所以go fmt.Println(<-ch1)里的chan接收操作是在main goroutine里执行的,因此死锁是板上钉钉的事情。

如果改成下面这样,死锁就不会发生:

package main

import (
"fmt"
"time"
) func main() {
ch1 := make(chan int)
go func() {
fmt.Println(<-ch1)
}()
ch1 <- 5
time.Sleep(1 * time.Second)
}

这是因为<-ch1这回货真价实地发生在了不同的goroutine里,死锁自然也不存在了。

这题很坏,坏就坏在fmt.Println(...)这样的形式容易让人迷惑,以为这个调用本身在新的goroutine里执行,然而真正在新goroutine里执行的却是fmt.Println内部的函数实现代码,而不是fmt.Println(...)这句,参数会在这之前就被求值。

那么这能让我们学到什么呢?答案是永远也不要写出题目里那样的代码,对于chan的操作应该确保是在和执行go语句的goroutine不同的routine中运行的。

不过万事不绝对,带buff的chan会有些例外,当然这些以后有机会再说吧:P

一道有趣的golang排错题的更多相关文章

  1. 一道有趣的for循环题

    一道有趣的for循环题 今天在复习js基础知识时发现了一个for循环的题,第一眼看到直接懵逼了,没想到for循环竟然还可以这样玩?涨姿势了. 题目是这样的 for(i=0, j=0; i<10, ...

  2. 一道有趣的javascript编程题

    题目:实现以下功能 1. 点击按钮“打开新窗口”,打开新的子页面,要求新窗口的大小为400px X 200px 2. 输入地址信息,点击“确定”按钮,关闭该页面 3. 将子页面中输入的地址信息,回传到 ...

  3. codeforces 1451D,一道有趣的博弈论问题

    大家好,欢迎来到codeforces专题. 今天选择的问题是Contest 1451场的D题,这是一道有趣简单的伪博弈论问题,全场通过的人有3203人.难度不太高,依旧以思维为主,坑不多,非常友好. ...

  4. 一道很经典的 BFS 题

    一道很经典的 BFS 题 想认真的写篇题解. 题目来自:https://www.luogu.org/problemnew/show/P1126 题目描述 机器人移动学会(RMI)现在正尝试用机器人搬运 ...

  5. ACM_一道耗时间的水题

    一道耗时间的水题 Time Limit: 2000/1000ms (Java/Others) Problem Description: Do you know how to read the phon ...

  6. 【图灵杯 F】一道简单的递推题(矩阵快速幂,乘法模板)

    Description 存在如下递推式: F(n+1)=A1*F(n)+A2*F(n-1)+-+An*F(1) F(n+2)=A1*F(n+1)+A2*F(n)+-+An*F(2) - 求第K项的值对 ...

  7. BZOJ2456-mode题解--一道有趣题

    题目链接: https://www.lydsy.com/JudgeOnline/problem.php?id=2456 瞎扯 这是今天考的模拟赛T2交互题的一个30分部分分,老师在讲题时提到了这题.考 ...

  8. Ural 1209. 1, 10, 100, 1000... 一道有趣的题

    1209. 1, 10, 100, 1000... Time limit: 1.0 secondMemory limit: 64 MB Let's consider an infinite seque ...

  9. 51nod1160 压缩算法的矩阵——一道有趣的题

    https://blog.csdn.net/lunch__/article/details/82655579 看似高大上,实际也不太好想到 先尝试确定一些位: 给出了最后一列,sort得到第一列 0X ...

随机推荐

  1. python叠加矩形框图层

    两种方式以及效果: 方式一,使用PIL.Image.blend方式: from PIL import Image, ImageDraw im = Image.open('d:/tmp/58.249.0 ...

  2. 《深入理解计算机系统》实验三 —— Buf Lab

    这是CSAPP的第三个实验,主要让我们熟悉GDB的使用,理解程序栈帧的结构和缓冲区溢出的原理. 实验目的   本实验的目的在于加深对IA-32函数调用规则和栈结构的具体理解.实验的主要内容是对一个可执 ...

  3. web前端js实现资源加载进度条

    进度条核心方法,通常j不考虑判断到100,根据项目中的图片数量可能有所差异所以到95就可以了 //根据图片load进度条 function loadingAsImgLength(){ var prec ...

  4. js第二次作业——2019.10.16

    第一题:完成省城市的三级联动(包括湖南省),附代码和效果图. 1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 </head> ...

  5. kali没有tcptraceroute如何安装

    问题描述 尝试使用kali进行路由信息的收集,发现kali没有自带tcptraceroute.在网上搜索教程发现都是Linux下安装,且都是使用yum安装,看了一下发现kali用的也不是yum 这就很 ...

  6. JDK 8 新特性,从入门到精通

    default关键字 在jdk1.8以前接口里面是只能有抽象方法,不能有任何方法的实现的. 在jdk1.8里面打破了这个规定,引入了新的关键字:default,使用default修饰方法,可以在接口里 ...

  7. [.NET] - EventLog.EntryWritten Event

    刚看到在MSND论坛上有人问一个EventLog.EntryWritten Event相关的问题,说是在2015触发了一个2013年的EventWritten的事件,比较好奇,然后查看了下这个类: h ...

  8. 关于 Softmax 回归的反向传播求导数过程

    对于 \(Softmax\) 回归的正向传播非常简单,就是对于一个输入 \(X\) 对每一个输入标量 \(x_i\) 进行加权求和得到 \(Z\) 然后对其做概率归一化. Softmax 示意图 下面 ...

  9. 英特尔® 图形性能分析器(Intel® GPA)

    英特尔图形性能分析器概述 英特尔 GPA 是一套软件工具,它能提供平台级游戏性能分析功能,优化应用性能. 英特尔 GPA 包含以下组件: 英特尔 GPA 监控器 - 将英特尔 GPA 连接至应用(处于 ...

  10. idea中快捷键换成熟悉的celipse中快捷键

    打开idea,找到菜单栏的file,点击打开,找到settings,打开   用key做关键词搜索keymap   找到之后点击打开,右侧就会显示快捷键界面,可以点击查看每一项 4 默认为defaul ...