【问题标题】:Unexpected Goroutine Behaviour意外的 Goroutine 行为
【发布时间】:2019-01-17 19:31:07
【问题描述】:

我是 Golang 的初学者

我正在从here. 阅读有关 Go 中的并发性

8th slide. 上向我提出问题之前,一切都很顺利
问题是:找出两个给定的二叉树是否等价。
我的方法:进行中序遍历,将两棵树的值保存在一个切片中并进行比较。

这是我的解决方案:[不完整]

package main

import (
    "fmt"
    "golang.org/x/tour/tree"
)

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
    if t != nil {
        Walk(t.Left, ch)
        ch <- t.Value
        Walk(t.Right, ch)
    }
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        fmt.Println("executing first go routing")
        Walk(t1, ch1)
        fmt.Println("closing channel [ch1]")
        close(ch1)
    }()

    go func() {
        fmt.Println("executing second go routing")
        Walk( t2, ch2 )
        fmt.Println("closing channel [ch2]")
        close(ch2)
    }()

    shouldContinue := true
    var continue1, continue2 bool
    for shouldContinue {
        select {
        case r1, ok1 := <-ch1:
            fmt.Println("[ch1] [rcvd]", r1)
            continue1 = ok1

        case r2, ok2 := <-ch2:
            fmt.Println("[ch2] [rcvd]", r2)
            continue2 = ok2
        }
        shouldContinue = continue1 || continue2
    }
    return true
}

func main() {
    Same(tree.New(1), tree.New(1))
}

我知道 goroutines 是协作调度的,如果它正在循环或连续进行计算,那么其中一个会完全阻塞另一个。所以我希望对于输出,它会首先从任一通道接收值,关闭它,然后它会从另一个通道接收值,然后关闭。一旦两者都关闭,for循环就会中断。

令我惊讶的是,第一个 go 例程从未安排好。这是我收到的输出:

executing second go routing
[ch2] [rcvd] 1
[ch2] [rcvd] 2
[ch2] [rcvd] 3
[ch2] [rcvd] 4
[ch2] [rcvd] 5
[ch2] [rcvd] 6
[ch2] [rcvd] 7
[ch2] [rcvd] 8
[ch2] [rcvd] 9
[ch2] [rcvd] 10
closing channel [ch2]
[ch2] [rcvd] 0 

谁能解释这里发生了什么?一旦 channel2 关闭并且第二个 goroutine 完成,为什么第一个不执行?

任何帮助将不胜感激。谢谢。

更新:
我在谷歌上搜索了关于打破频道的信息,我发现了一个 SO 问题here. 据此,我将我的解决方案更新如下:

package main

import (
    "fmt"
    "golang.org/x/tour/tree"
    // "time"
)

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
    // time.Sleep(time.Millisecond)
    if t != nil {
        Walk(t.Left, ch)
        ch <- t.Value
        Walk(t.Right, ch)
    }
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        fmt.Println("executing first go routing")
        Walk(t1, ch1)
        fmt.Println("closing channel [ch1]")
        close(ch1)
    }()

    go func() {
        fmt.Println("executing second go routing")
        Walk( t2, ch2 )
        fmt.Println("closing channel [ch2]")
        close(ch2)
    }()

    for {
        select {
        case r1, ok1 := <-ch1:
            fmt.Println("[ch1] [rcvd]", r1)
            if !ok1 {
                ch1 = nil
            } 

        case r2, ok2 := <-ch2:
            fmt.Println("[ch2] [rcvd]", r2)
            if !ok2 {
                ch2 = nil
            }
        }
        if ch1 == nil && ch2 == nil {
            break
        }
    }
    return true
}

func main() {
    Same(tree.New(1), tree.New(1))
}

这给出了我认为第一个 sn-p 会的确切输出:

executing second go routing
[ch2] [rcvd] 1
[ch2] [rcvd] 2
[ch2] [rcvd] 3
[ch2] [rcvd] 4
[ch2] [rcvd] 5
[ch2] [rcvd] 6
[ch2] [rcvd] 7
[ch2] [rcvd] 8
[ch2] [rcvd] 9
[ch2] [rcvd] 10
closing channel [ch2]
[ch2] [rcvd] 0
executing first go routing
[ch1] [rcvd] 1
[ch1] [rcvd] 2
[ch1] [rcvd] 3
[ch1] [rcvd] 4
[ch1] [rcvd] 5
[ch1] [rcvd] 6
[ch1] [rcvd] 7
[ch1] [rcvd] 8
[ch1] [rcvd] 9
[ch1] [rcvd] 10
closing channel [ch1]
[ch1] [rcvd] 0

我现在对发生的事情更加困惑。

【问题讨论】:

标签: go goroutine


【解决方案1】:

一旦channel2关闭,为什么第一个不执行?

不执行频道。一遍又一遍地执行的是您的选择。请注意,无论通道是否关闭,这两种情况都可以始终执行。因此 select 可以选择 id 所做的第二种情况并且您中止了。 (你的中止条件看起来很可疑:一旦 两个 通道都关闭,即如果 ok1 和 ok2 都为假,你就完成了。

不要将 select 本身视为“goroutine 调度工具”。它不是。它将随机选择一种 runnable 案例。如果您所有的案例都是val, ok := &lt;- ch 的形式,那么所有案例都是可运行的,并且 select 可能总是选择第二个。或者第一个,或者...

[第二个解决方案]我现在对发生的事情更加困惑。

您的中止条件不同。一旦两个通道都关闭,您就会中断,这会在两个通道都关闭时发生。这与您的第一个解决方案不同,因为一旦 any one 频道关闭,第一个解决方案就会中断。

这里的并发问题不在于 goroutine 调度,而仅仅是 for 循环执行 select 的中止条件。它们与第一个和第二个不同,第一个从根本上是错误的,因为一旦任何通道耗尽,它就会停止。

【讨论】:

  • 感谢您的努力。 为什么第一个不被执行?是我在问题后面更正的错字
【解决方案2】:

在您的代码的第一部分,您的逻辑存在错误。

shouldContinue := true
var continue1, continue2 bool
for shouldContinue {
    select {
    case r1, ok1 := <-ch1:
        fmt.Println("[ch1] [rcvd]", r1)
        continue1 = ok1

    case r2, ok2 := <-ch2:
        fmt.Println("[ch2] [rcvd]", r2)
        continue2 = ok2
    }
    shouldContinue = continue1 || continue2
}

在上面的代码中,continue1continue2falseselect 正在阻止,直到他的一个案例得到满足。让我们说case r2, ok2 := &lt;-ch2: 首先实现,然后continue2 将是true。因为shouldContinue = continue1 || continue2这个条件,for循环会继续。出于某种原因(进行例行调度)case r2, ok2 := &lt;-ch2: 条件每次都满足。现在当ch2 关闭时,ok2 的值将是false,所以continue2 也将是false。现在,continue1continue2 都是false,所以shouldContinue 也将是false。因此它打破了for 循环,您无法看到ch1 的输出。试试这个:

continue1 = true
continue2 = true
for shouldContinue {
    select {
    case r1, ok1 := <-ch1:
        fmt.Println("[ch1] [rcvd]", r1)
        continue1 = ok1

    case r2, ok2 := <-ch2:
        fmt.Println("[ch2] [rcvd]", r2)
        continue2 = ok2
    }
    shouldContinue = continue1 || continue2
}

当通道关闭时,您无法在该通道上发送值,但您仍然可以从该通道接收。见这里:https://play.golang.org/p/S4USguWfN_z

Nil 频道 始终处于阻塞状态,并且您更改了 for 循环中断逻辑。这就是您的第二个解决方案有效的原因。

【讨论】:

  • 谢谢。这个答案也很有帮助。 +1
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-04
  • 2016-07-16
  • 2016-05-10
  • 2020-07-23
  • 2021-08-23
  • 2021-11-16
相关资源
最近更新 更多