为什么闭包中局部分配的变量在外部分配时会有所不同?
Why does a locally allocated variable in a closure works differently when allocated outside?
我在这样的函数中有一个闭包:
func permutate(ch chan []int, numbers []int, r int) {
// ... see the full program below
perm := make([]int, r, r)
nextPerm := func() []int {
for i, ind := range indices[:r] {
perm[i] = numbers[ind]
}
return perm
}
// later writing to ch in two places:
// ch <- nextPerm()
// ...
}
当我在闭包中分配 perm
变量时,这会有所不同:
func permutate(ch chan []int, numbers []int, r int) {
// ...
nextPerm := func() []int {
perm := make([]int, r, r)
for i, ind := range indices[:r] {
perm[i] = numbers[ind]
}
return perm
}
// ...
}
我不明白为什么。这两种变体有什么区别?
我只在一个 goroutine 中 运行 permutate
,所以写入通道应该以串行方式进行,所以两个 goroutine 不应该同时修改 perm
变量。
我试图调试发生了什么,但我猜这是一个 Heisenbug 因为在调试期间,竞争条件不会发生,所以我猜它与 goroutines 的调度有关。
这是完整的程序(带有全局 perm
变量):
package main
import (
"errors"
"fmt"
)
func IterPermutations(numbers []int, r int) <-chan []int {
if r > len(numbers) {
err := errors.New("r cannot be bigger than the length of numbers")
panic(err)
}
ch := make(chan []int)
go func() {
defer close(ch)
permutate(ch, numbers, r)
}()
return ch
}
// an implementation similar to Python standard library itertools.permutations:
// https://docs.python.org/3.8/library/itertools.html#itertools.permutations
func permutate(ch chan []int, numbers []int, r int) {
n := len(numbers)
if r < 0 {
r = n
}
indices := make([]int, n, n)
for i := 0; i < n; i++ {
indices[i] = i
}
cycles := make([]int, r, r)
for i := 0; i < r; i++ {
cycles[i] = n - i
}
perm := make([]int, r, r)
nextPerm := func() []int {
for i, ind := range indices[:r] {
perm[i] = numbers[ind]
}
return perm
}
ch <- nextPerm()
if n < 2 {
return
}
var tmp []int
var j int
for i := r - 1; i > -1; i-- {
cycles[i] -= 1
if cycles[i] == 0 {
tmp = append(indices[i+1:], indices[i])
indices = append(indices[:i], tmp...)
cycles[i] = n - i
} else {
j = len(indices) - cycles[i]
indices[i], indices[j] = indices[j], indices[i]
ch <- nextPerm()
i = r // start over the cycle
// i-- will apply, so i will be r-1 at the start of the next cycle
}
}
}
func main() {
for perm := range IterPermutations(phaseSettings, 3) {
fmt.Println(perm)
}
}
这是一场数据竞赛。当您在闭包外部声明 perm
时,闭包会在每次调用和修改它时重新使用 perm
。
在主 goroutine 通过通道接收切片后,permutate
goroutine 可以保持 运行 并调用下一个 nextPerm()
- 修改切片,如前所述。这可能会也可能不会发生在主 goroutine 使用它之前(甚至发生在某些事情的中间),这是一场数据竞争。所以 fmt.Println(perm)
可能会打印下一个排列或正确的排列(或者在极少数情况下,两个排列的混合)。
当您在闭包内部声明 perm
时,它是一个新变量,并且每次调用闭包时都会重新分配基础数组。因此,不会共享任何内容,也不会争用任何数据。
注意:Go 的竞争检测器可能无法每次都检测到数据竞争——因为数据竞争可能根本不会每次都发生。要了解有关竞争检测器的更多信息,请参阅 https://blog.golang.org/race-detector and https://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm。
我在这样的函数中有一个闭包:
func permutate(ch chan []int, numbers []int, r int) {
// ... see the full program below
perm := make([]int, r, r)
nextPerm := func() []int {
for i, ind := range indices[:r] {
perm[i] = numbers[ind]
}
return perm
}
// later writing to ch in two places:
// ch <- nextPerm()
// ...
}
当我在闭包中分配 perm
变量时,这会有所不同:
func permutate(ch chan []int, numbers []int, r int) {
// ...
nextPerm := func() []int {
perm := make([]int, r, r)
for i, ind := range indices[:r] {
perm[i] = numbers[ind]
}
return perm
}
// ...
}
我不明白为什么。这两种变体有什么区别?
我只在一个 goroutine 中 运行 permutate
,所以写入通道应该以串行方式进行,所以两个 goroutine 不应该同时修改 perm
变量。
我试图调试发生了什么,但我猜这是一个 Heisenbug 因为在调试期间,竞争条件不会发生,所以我猜它与 goroutines 的调度有关。
这是完整的程序(带有全局 perm
变量):
package main
import (
"errors"
"fmt"
)
func IterPermutations(numbers []int, r int) <-chan []int {
if r > len(numbers) {
err := errors.New("r cannot be bigger than the length of numbers")
panic(err)
}
ch := make(chan []int)
go func() {
defer close(ch)
permutate(ch, numbers, r)
}()
return ch
}
// an implementation similar to Python standard library itertools.permutations:
// https://docs.python.org/3.8/library/itertools.html#itertools.permutations
func permutate(ch chan []int, numbers []int, r int) {
n := len(numbers)
if r < 0 {
r = n
}
indices := make([]int, n, n)
for i := 0; i < n; i++ {
indices[i] = i
}
cycles := make([]int, r, r)
for i := 0; i < r; i++ {
cycles[i] = n - i
}
perm := make([]int, r, r)
nextPerm := func() []int {
for i, ind := range indices[:r] {
perm[i] = numbers[ind]
}
return perm
}
ch <- nextPerm()
if n < 2 {
return
}
var tmp []int
var j int
for i := r - 1; i > -1; i-- {
cycles[i] -= 1
if cycles[i] == 0 {
tmp = append(indices[i+1:], indices[i])
indices = append(indices[:i], tmp...)
cycles[i] = n - i
} else {
j = len(indices) - cycles[i]
indices[i], indices[j] = indices[j], indices[i]
ch <- nextPerm()
i = r // start over the cycle
// i-- will apply, so i will be r-1 at the start of the next cycle
}
}
}
func main() {
for perm := range IterPermutations(phaseSettings, 3) {
fmt.Println(perm)
}
}
这是一场数据竞赛。当您在闭包外部声明 perm
时,闭包会在每次调用和修改它时重新使用 perm
。
在主 goroutine 通过通道接收切片后,permutate
goroutine 可以保持 运行 并调用下一个 nextPerm()
- 修改切片,如前所述。这可能会也可能不会发生在主 goroutine 使用它之前(甚至发生在某些事情的中间),这是一场数据竞争。所以 fmt.Println(perm)
可能会打印下一个排列或正确的排列(或者在极少数情况下,两个排列的混合)。
当您在闭包内部声明 perm
时,它是一个新变量,并且每次调用闭包时都会重新分配基础数组。因此,不会共享任何内容,也不会争用任何数据。
注意:Go 的竞争检测器可能无法每次都检测到数据竞争——因为数据竞争可能根本不会每次都发生。要了解有关竞争检测器的更多信息,请参阅 https://blog.golang.org/race-detector and https://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm。