使用Golang通过通道进行范围遍历,在实现Heap的排列算法时出现奇怪的行为。

huangapple go评论89阅读模式
英文:

Golang range through channel with odd behaviour when inplementing Heap's permutation algorithm

问题

我正在为你翻译以下内容:

我试图使用通道在Go语言中实现Heap's算法。下面的代码在仅仅将切片打印到屏幕上时工作正常,但是当使用通道将数组传递给主函数中的for/range循环时,出现了一些意外的行为,切片/数组被重复打印,并且并没有发送所有的排列。我认为可能是我在主函数打印结果之前关闭了通道,但我不希望出现重复打印。为什么会发生这种情况,我该如何解决?

package main

import "fmt"

func perm(a []int64) {
    var n = len(a)
    var c = make([]int, n)
    fmt.Println(a)
    i := 0
    for i < n {
        if c[i] < i {
            if i%2 == 0 {
                a[0], a[i] = a[i], a[0]
            } else {
                a[c[i]], a[i] = a[i], a[c[i]]
            }
            fmt.Println(a)
            c[i]++
            i = 0
        } else {
            c[i] = 0
            i++
        }
    }
}

func permch(a []int64, ch chan<- []int64) {
    var n = len(a)
    var c = make([]int, n)
    ch <- a
    i := 0
    for i < n {
        if c[i] < i {
            if i%2 == 0 {
                a[0], a[i] = a[i], a[0]
            } else {
                a[c[i]], a[i] = a[i], a[c[i]]
            }
            ch <- a
            c[i]++
            i = 0
        } else {
            c[i] = 0
            i++
        }
    }
    close(ch)
}

func main() {
    var i = []int64{1, 2, 3}
    fmt.Println("Without Channels")
    perm(i)
    ch := make(chan []int64)
    go permch(i, ch)
    fmt.Println("With Channels")
    for slc := range ch {
        fmt.Println(slc)
    }
}

你可以尝试将通道的关闭操作放在主函数中的for循环之后,看看是否能解决重复打印的问题。

英文:

I was trying to implement Heap's Algorithm in go using channels. The code below is working fine when just printing the slices on the screen, but when using channels to delivery the arrays to a for/range loop on main function some unexpected behaviour occurs and the slices/arrays are printed in duplicity and not all permutations are sent. I thought that maybe i'm closing the channel earlier than the main function is able to print the results but i wouldn't expect that double print. Why is this happening and how can i make it work.

package main
import &quot;fmt&quot;
func perm(a []int64) {
var n = len(a)
var c = make([]int, n)
fmt.Println(a)
i := 0
for i &lt; n {
if c[i] &lt; i {
if i%2 == 0 {
a[0], a[i] = a[i], a[0]
} else {
a[c[i]], a[i] = a[i], a[c[i]]
}
fmt.Println(a)
c[i]++
i = 0
} else {
c[i] = 0
i++
}
}
}
func permch(a []int64, ch chan&lt;- []int64) {
var n = len(a)
var c = make([]int, n)
ch &lt;- a
i := 0
for i &lt; n {
if c[i] &lt; i {
if i%2 == 0 {
a[0], a[i] = a[i], a[0]
} else {
a[c[i]], a[i] = a[i], a[c[i]]
}
ch &lt;- a
c[i]++
i = 0
} else {
c[i] = 0
i++
}
}
close(ch)
}
func main() {
var i = []int64{1, 2, 3}
fmt.Println(&quot;Without Channels&quot;)
perm(i)
ch := make(chan []int64)
go permch(i, ch)
fmt.Println(&quot;With Channels&quot;)
for slc := range ch {
fmt.Println(slc)
}
}

答案1

得分: 2

你的问题在于切片是引用类型,并且在多个 goroutine 中被访问。在 perm 函数中,你在每一步完成处理后直接打印 a。在 permch 函数中,你将 a 发送到通道中,但立即开始修改它。由于每个发送到通道的切片都引用相同的底层数组,所以在下一次循环迭代修改 a 或者主函数中的 Println() 调用先访问该数组之间存在竞争条件。

一般来说,如果在使用 goroutine 的任何程序中遇到意外行为,很可能是存在竞争条件。可以使用 -race 标志运行程序以查看问题出在哪里。

编辑: 另外,关闭一个通道对于从通道中读取的例程没有影响。通道可以继续读取,直到其缓冲区为空,此时它将开始返回该类型的零值。使用 range 循环遍历通道只有在通道关闭且其缓冲区为空时才会终止。

英文:

Your problem is that slices are reference types, and are being accessed in multiple goroutines. In perm, you're printing a directly as you finish processing it at each step. In permch, you're sending a over the channel, but then immediate starting to modify it again. Since each slice sent over the channel refers to the same underlying array, you have a race condition as to whether your next loop iteration alters a or your Println() call in main gets to that array first.

In general, if you're running into unexpected behavior in any program using goroutines, you probably have a race condition. Run the program with the -race flag to see where.

Edit: also, closing a channel has no effect on a routine reading from the channel. A channel can continue to be read until its buffer is empty, at which point it will start returning the zero value for that type instead. Range loops over a channel will only terminate once the channel is closed and its buffer is empty.

答案2

得分: 0

看起来permchmain打印a的同时修改了它,所以输出结果混乱。

我可以想到三种简单的修复方法:

  1. 使用互斥锁保护对a的访问。
  2. 在通道上放置a的副本:
  3. 从主函数返回一个信号,表示已经打印完毕,permch可以继续执行。(不太推荐,但是可以工作)

第二种方法非常简单:

a2 := make([]int64, len(a))
copy(a2, a)
ch <- a2

这是我推荐的方法。

对于第一种方法,只需在包级别声明一个var m sync.Mutex,并在读取或修改a时进行锁定。不过,正如你指出的,这会导致读取者和下一次修改之间的竞争条件,所以这可能并不是一个好的解决方案。

在 playground 上使用第三种方法修复的版本

英文:

It looks like permch is modifying a at the same time as main is printing it, so your output is garbled.

I can think of three easy fixes:

  1. Guard access to a with a mutex.
  2. Put a copy of a on the channel:
  3. Have some kind of return signal from main that it has printed and permch can continue. (don't really recommend this, but it works).

Number 2 is pretty simple:

a2 := make([]int64, len(a))
copy(a2,a)
ch &lt;- a2

and is what I would recommend.

For #1, just declare a var m sync.Mutex as a package variable and Lock is anytime you read or modify a. This is a race condition though between the reader and the next modification, as you pointed out, so it probably isn't a good solution after all.

fixed version on playground using #3

huangapple
  • 本文由 发表于 2017年5月2日 00:24:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/43722558.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定