一个select死锁问题

话说前几天我遇到了一个死锁问题,当时想了一些办法糊弄过去了,不过并没有搞明白问题的细节,周末想起来便继续研究了一下,最终便有了这篇文章。

让我们搞一段简单的代码来重现一下当时我遇到的问题:

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	foo := make(chan int)
	bar := make(chan int)
	closing := make(chan struct{})
	wg.Add(1)
	go func() {
		defer wg.Done()
		select {
		case foo <- <-bar:
		case <-closing:
			println("closing")
		}
	}()
	// bar <- 123
	close(closing)
	wg.Wait()
}

运行后报错,提示死锁:

fatal error: all goroutines are asleep – deadlock!

因为「foo <- <-bar」的写法不太常见,所以第一感觉是不是 select 的 case 语句只能操作一个 chan,不能同时操作多个 chan,于是我改了一下,每个 case 只读写一个 chan:

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	foo := make(chan int)
	bar := make(chan int)
	closing := make(chan struct{})
	wg.Add(1)
	go func() {
		defer wg.Done()
		select {
		case v := <-bar:
			foo <- v
		case <-closing:
			println("closing")
		}
	}()
	close(closing)
	wg.Wait()
}

果然死锁消失了!似乎 select 中,每个 case 确实只能读写一个 chan。为了确认到底是不是这个原因,我又修改了一下最初有问题的代码,加上了「bar <- 123」,结果死锁也消失了。看来虽然我找到了解决问题的方法,但是并没有找到解释问题的原因。

周末在家躺在床上,想起我认识的一个 golang 大神总对我说的:一切问题的答案都在 spec 里。于是挣扎着爬起来仔细翻阅关于 select 的说明,终于发现了问题真正的原因:

For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the “select” statement. The result is a set of channels to receive from or send to, and the corresponding values to send. Any side effects in that evaluation will occur irrespective of which (if any) communication operation is selected to proceed. Expressions on the left-hand side of a RecvStmt with a short variable declaration or assignment are not yet evaluated.

结合这段话,让我们再来看看 case 中的这行代码「foo <- <-bar」,因为所有 chan 表达式都会被求值、所有被发送的表达式都会被求值,所以右手边表达式(<-bar)会被先执行,如果拿到结果后再选择 case 执行,如果拿不到结果就会一直堵塞,于是死锁。

如果你还是想不明白,那么你可以认为问题代码实际上等价于如下的写法:

package main

import "sync"

func main() {
	var wg sync.WaitGroup
	foo := make(chan int)
	bar := make(chan int)
	closing := make(chan struct{})
	wg.Add(1)
	go func() {
		defer wg.Done()
		v := <-bar
		select {
		case foo <- v:
		case <-closing:
			println("closing")
		}
	}()
	// bar <- 123
	close(closing)
	wg.Wait()
}

如果你觉得自己已经完全明白了,那么不妨看看下面这段代码:

package main

import (
	"fmt"
	"time"
)

func talk(msg string, sleep int) <-chan string {
	ch := make(chan string)
	go func() {
		for i := 0; i < 5; i++ {
			ch <- fmt.Sprintf("%s %d", msg, i)
			time.Sleep(time.Duration(sleep) * time.Millisecond)
		}
	}()
	return ch
}

func fanIn(input1, input2 <-chan string) <-chan string {
	ch := make(chan string)
	go func() {
		for {
			select {
			case ch <- <-input1:
			case ch <- <-input2:
			}
		}
	}()
	return ch
}

func main() {
	ch := fanIn(talk("A", 10), talk("B", 1000))
	for i := 0; i < 10; i++ {
		fmt.Printf("%q\n", <-ch)
	}
}

当然会出现死锁,我的问题是为什么每次都是不多不少输出一半数据才死锁?请回答。

一个select死锁问题》上有11个想法

  1. 王老师好,最后这个问题,我认为是某个 talk 函数正好五次循环执行结束了,goruntine 退出,然后就没有 goruntine 继续写入 ch ,导致 case 右边计算的时候卡住了,select 本身回不停的循环检查,不会卡住,但是右边的 <-input1 回卡住。

    我有一个疑问,这里是2个 gorutine, 分别通过 msg 和 i 格式化字符串,但是输出的 i 为什么是递增的,暗道理,每个 goroutine 都有自己的局部变量 i
    "B 0"
    "A 1"
    "B 2"
    "A 3"
    "B 4"
    这里我认为应该是 A 0 开始,实际雀不是这样,这是为什么。

    • 原题出处:https://stackoverflow.com/questions/51167940/chained-channel-operations-in-a-single-select-case

    • 按照作者后面重写的方式理解就很清晰了,每次for循环后,<-input1 <-input2 的值都被重新计算了,所以是递增的

    • 至于为什么会卡住,也应该是因为 input1 input2 并没有被close掉,导致阻塞在fanIn里了,在talk的 go func里面加上close程序就能正确输出了

  2. 上面的同学回答的已经很清楚了,关于为什么序号是递增的,我想是因为两个goroutine是并行执行所以速度是一样的值也是一样的从0~4,但是同一时刻只能有一个能发送到channel里面,所以才是递增输出的。

  3. 你把5改为4,它就不多不少执行4次,然后就死锁了。只不过5恰好是10的一半而言。对于发生死锁的问题,你的文章已经解释过了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注