这段代码有什么问题及何解?

代码如下:

// case2_test.go
package case2

import "testing"

func TestCase2(t *testing.T) {
   ch1 := make(chan int)
   ch2 := make(chan int)

   for i := 0; i < 9; i++ {
      go func(i int) {
         ch1 <- i
      }(i)
   }

   for i := 0; i < 10; i++ {
      go func(i int) {
         ch2 <- i
      }(i)
   }

   for {
      select {
      case x1 := <-ch1:
         t.Logf("x1 = %d", x1)
      case x2 := <-ch2:
         t.Logf("x2 = %d", x2)
      default:
         t.Logf("break")
         break
  }
   }
}

没有 default 时报错如下:
fatal error: all goroutines are asleep - deadlock!
有 default 时, 执行测试, 内存会一直增加, 而没有任何输出!
哪位能给解答下嘛?

go
觉得还不错点个赞呗!
taadis
讨论数量: 2
pardon110

无default时,通道无法正常读写,运行时监测到此情况抛出goroutine死锁异常退出。select是随机执行任意分支,在你使用了defaut时,就变成了一个无阻塞循环输出default内容。问题的关键在于主goroutine(函数main)要留出一定的时间,让通道所在的goroutine有机会执行。所以需要用到sleep与goto,前者是为了阻塞,后者是防止给的时间片被随机叠加,用完了直接跳出循环。代码如下

    for {
        select {
        case x1 := <-ch1:
            t.Logf("x1 = %d", x1)
        case x2 := <-ch2:
            t.Logf("x2 = %d", x2)
        default:
            time.Sleep(1 * time.Millisecond)
            goto LOOP
        }
    }

LOOP:
    t.Logf("break")
}
4年前 评论

首先在我的电脑上运行的结果是这样的,简化了一下都循环两次。go版本1.14。
file

file

file

当没有default时,goroutine取出无缓冲通道中唯一的值后,无缓冲通道将变为空,之后任何尝试从空通道获取值的goroutine都将被阻塞并进入休眠状态。在这里,for循环中x1和x2想分别从通道ch1和通道ch2取出值,都会等待有一个goroutine在相应的通道中放入值。而放入值以后,下一个循环的goroutine想要放入值,必须等待这个通道的值在for循环中被取出。然而,我们两个通道只会被放入两次值,分别被放入两次值以后,放入值的goroutine已经全都执行完毕。通道ch1和ch2将永远不会再有值,而for循环只能永远的等待下去。在检测到这一情况后,以前版本的Go运行时环境会抛出死锁错误。(1.14版本没有报,而只是阻塞,具体哪个版本修改的待查阅)。

当有default时,我们只看到默认分支打印的输出:这是因为程序调用select太早了,以至于通道ch1和ch2还没来得及接受goroutine传给它们的值。需要在每次迭代之前加上1μs的延迟,从而使通道能够正常接收goroutine发送给它们的值。比如下面这样:

for i := 0; i < 5; i++ {
        time.Sleep(1 * time.Microsecond)
        select {
        case x1 := <-ch1:
            fmt.Printf("x1 = %d\n", x1)
        case x2 := <-ch2:
            fmt.Printf("x2 = %d\n", x2)
        default:
            fmt.Println("Default")
        }
    }

另外,break在go中可以跳出for, switch和select,所以这里跳出的是select,而不是无限循环。

而且很多时候,一般会将通道作为参数传给函数;使用等待组等待所有goroutine结束防止程序比goroutine更早结束;使用关闭通道(close(c))来退出带有select语句的无限循环。比如下面这样的方式退出无限循环,通道被关闭后ok1、ok2将被设为false

for ok1 || ok2{
        select {
        case msg, ok1 = <-a:
            if ok1 {
                fmt.Printf("%s from A\n", msg)
            }
        case msg, ok2 = <-b:
            if ok2 {
                fmt.Printf("%s from B\n", msg)
            }
        }
    }
4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!