【问题标题】:Weird output with the number of active goroutines带有活动 goroutine 数量的奇怪输出
【发布时间】:2020-12-25 09:43:59
【问题描述】:

我试图通过运行小程序来学习频道。我不明白下面的程序,因为它给出了一个奇怪的输出:

func squares(c chan int) {
    for i := 0; i < 4; i++ {
        num := <-c
        fmt.Println(num * num)
    }
}

func main() {
    c := make(chan int, 3)
    go squares(c)

    c <- 1
    c <- 2
    c <- 3
    c <- 4

    fmt.Println(runtime.NumGoroutine())
    time.Sleep(time.Second)
    fmt.Println(runtime.NumGoroutine())
}

在执行这个程序时,我看到第一次它打印活动 goroutine 的数量为 2。然后一秒钟后,它的输出为 1。这真的很奇怪。

我查了几个博客,但无法理解。那么在 goroutine 停止工作的那一秒内到底发生了什么变化?

【问题讨论】:

  • 第二个 goroutine 在工作完成后的某个时刻“停止工作”。你给了它一个非常短的任务,你的计算机可以在不到一秒的时间内完成这并不奇怪。
  • runtime.NumGoroutine 报告所有 goroutine,不仅包括您启动的那些,还包括运行时 itslef 使用的那些。这里没有什么可看或学到的。

标签: go channel


【解决方案1】:

goroutine 的顺序无法保证。根据您的观察,看起来在写入通道后,主 goroutine 继续并将活动的 goroutine 打印为两个,然后 squares goroutine 运行并完成。

你也有可能从第一次调用中得到 1 来获取 goroutine 的数量。如果squares goroutine 在主 goroutine 写入通道后立即运行,就会发生这种情况。您可以通过在写入通道之后但在获取 goroutine 数量之前添加睡眠调用来强制执行此操作。

【讨论】:

    【解决方案2】:

    您的第二个 goroutine 接收 4 个值,打印它们并几乎立即退出(当squares 返回时)。您很幸运,它在第一次调用 NumGoRoutines 时仍在运行。

    顺便说一句,我建议您阅读有关该语言的信息,而不是通过实验来学习。仅仅因为某些东西在您尝试时有效,并不意味着它是正确的方法并且永远有效,尤其是在涉及并发的情况下。或者,使用竞态检测器显示任何数据竞态。

    【讨论】:

      【解决方案3】:
      1. 让我强制执行排序,这样你甚至不需要计算 goroutine 的数量,试试this
      package main
      
      import (
          "fmt"
          "runtime"
          "sync"
      )
      
      func main() {
          var wg sync.WaitGroup
          ch := make(chan int)
          wg.Add(1)
          go func() {
              defer wg.Done()
              for n := range ch {
                  fmt.Println(n)
              }
          }()
      
          ch <- 1
          ch <- 2
          ch <- 3
          ch <- 4
      
          fmt.Println("NumGoroutine =", runtime.NumGoroutine()) // 2
          close(ch)
          wg.Wait()
          fmt.Println("NumGoroutine =", runtime.NumGoroutine()) // 1
      }
      
      

      输出:

      1
      2
      3
      NumGoroutine = 2
      4
      NumGoroutine = 1
      

      注意事项:

      1. 使用无缓冲通道强制一对一同步写入到通道和读取通道。
      2. 显式关闭通道以退出for 循环。
      3. 使用wg.Done() 明确表示goroutine 的结束,所以wg.Wait() 等待它。

      1. 让我先展开for循环:
      func squares(c chan int) {
          num := <-c
          fmt.Println(num * num)
          num = <-c
          fmt.Println(num * num)
          num = <-c
          fmt.Println(num * num)
          num = <-c
          fmt.Println(num * num)
      }
      func main() {
          c := make(chan int, 3)
          go squares(c)
          c <- 1
          c <- 2
          c <- 3
          c <- 4
          fmt.Println("NumGoroutine =", runtime.NumGoroutine())
          time.Sleep(100 * time.Millisecond)
          fmt.Println("NumGoroutine =", runtime.NumGoroutine())
      }
      
      

      现在让我们运行它:
      由于您使用的是 3 个缓冲通道,因此这 3 行运行速度很快(这意味着无需等待通道同步 - 只需将 1, 2, 3 附加到通道的缓冲区):

          c <- 1
          c <- 2
          c <- 3
      

      根据操作系统和 CPU 负载,消费者 goroutine 可能会全部消耗或不消耗,
      在这个c &lt;- 4 点,我们有两种情况:

      1. 通道已满:所以main goroutine 等待通道同步。
      2. 通道有一个(或多个)空闲位置:因此c &lt;- 4 运行速度很快,只需将4 放入通道的缓冲区中(无需等待通道同步)。

      现在在c &lt;- 4 点之后,我们也有两种情况:

      1. 另一个 goroutine 使用 4 并退出,所以我们只有 一个 main goroutine。
      2. 另一个goroutine使用4,所以我们有两个 goroutine。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-08
        • 1970-01-01
        • 1970-01-01
        • 2014-10-20
        • 2014-04-30
        相关资源
        最近更新 更多