【问题标题】:Why does a locally allocated variable in a closure works differently when allocated outside?为什么闭包中本地分配的变量在外部分配时的工作方式不同?
【发布时间】:2020-04-01 06:28:05
【问题描述】:

我在这样的函数中有一个闭包:

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)
    }
}

【问题讨论】:

  • 在第一个示例中,从函数返回的所有切片共享相同的后备数组。使用race detector 运行应用程序。
  • 我使用非常简单的测试用例(0、1 和 2 个元素)运行,但没有比赛。

标签: go closures channel goroutine


【解决方案1】:

这是一场数据竞赛。当您在闭包之外声明 perm 时,闭包会在每次调用和修改它时重新使用 perm

在主 goroutine 通过通道接收到切片后,permutate goroutine 可以继续运行并调用下一个 nextPerm() - 修改切片,正如解释的那样。这可能会在主 goroutine 使用它之前发生,也可能不会发生(甚至发生在某些事情的中间),这是一场数据竞争。所以fmt.Println(perm) 可能会打印下一个排列迭代或正确的排列(或者在极少数情况下,混合两个排列)。

当您在闭包内声明perm 时,它是一个新变量,并且每次调用闭包时都会分配新的底层数组。所以没有任何东西被共享,也没有数据被竞争。

注意:Go 的竞争检测器可能无法每次都检测到数据竞争 - 因为数据竞争可能不会每次都发生。要了解有关比赛检测器的更多信息,请参阅 https://blog.golang.org/race-detectorhttps://github.com/google/sanitizers/wiki/ThreadSanitizerAlgorithm

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-12-29
    • 1970-01-01
    • 2012-08-18
    • 1970-01-01
    • 2021-01-12
    • 2012-08-12
    • 1970-01-01
    • 2015-12-23
    相关资源
    最近更新 更多