作为 goroutine 在循环变量上启动方法调用的意外行为

Unexpected behavior from launching a method call on a loop variable as a goroutine

我读了这个 article 并决定自己重复这种行为并进行试验:

package main

import (
    "fmt"
    "time"
)

type User struct {
    i    int
    token string
}

func NewUser(i int, token string) User {
    user := User{token: fmt.Sprint(i), i: i}
    return user
}

func (u *User) PrintAddr() {
    fmt.Printf("%d (PrintAddr): %p\n", u.i, u)
}

func main() {
    users := make([]User, 4)
    for i := 0; i < 4; i++ {
        user := NewUser(i, "")
        users[i] = user
    }
    
    for i, user := range users {
        go user.PrintAddr()
        go users[i].PrintAddr()
    }
    time.Sleep(time.Second)
}

(Playground)

代码输出如下:

1 (PrintAddr): 0xc000056198
2 (PrintAddr): 0xc0000561b0
0 (PrintAddr): 0xc000056180
3 (PrintAddr): 0xc00000c030
3 (PrintAddr): 0xc00000c030
3 (PrintAddr): 0xc00000c030
3 (PrintAddr): 0xc00000c030
3 (PrintAddr): 0xc0000561c8

我也不明白,为什么5个3 (PrintAddr)中有4个是0xc00000c030,最后一个不一样?


但是,如果我使用 pointer 数组而不是 value 数组,像这样,

func NewUser(i int, token string) *User {
    user := &User{token: fmt.Sprint(i), i: i}
    return user
}
// -snip-
func main() {
    users := make([]*User, 4)
    // -snip-

(Playground)

那么这里一切都很好,每个条目都使用相同的地址打印了两次:

1 (PrintAddr): 0xc0000ae030
3 (PrintAddr): 0xc0000ae060
2 (PrintAddr): 0xc0000ae048
2 (PrintAddr): 0xc0000ae048
3 (PrintAddr): 0xc0000ae060
1 (PrintAddr): 0xc0000ae030
0 (PrintAddr): 0xc0000ae018
0 (PrintAddr): 0xc0000ae018

但是为什么文章中的情况在这里不适用,我没有得到很多3 (PrintAddr)呢?

代码的第一个版本超过值 slicetaking the address of the iterator variable.。为什么?

方法PrintAddr定义在指针接收者上:

func (u *User) PrintAddr() {
    fmt.Printf("%d (PrintAddr): %p\n", u.i, u)
}

在 for 循环中,user 迭代变量在每个循环中重复使用,并在切片中分配下一个值。因此它是同一个变量。但是你通过调用在指针接收器上定义的方法来获取它的地址:

    users := make([]User, 4)
    // ...
    for i, user := range users {
        go user.PrintAddr()
        go users[i].PrintAddr()
    }

Calling 值等于 (&user).PrintAddr():

的方法

If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m()

索引切片反而会按预期工作,因为您正在访问切片中的第 i 个实际值,而不是使用迭代器变量。

更改切片以保存指针值也可以解决此问题,因为迭代器变量现在是指向 User 值的指针的副本。

问题

您的第一个版本有一个同步错误,表现为数据竞争:

$ go run -race main.go
0 (PrintAddr): 0xc0000b4018
0 (PrintAddr): 0xc0000c2120
==================
WARNING: DATA RACE
Write at 0x00c0000b4018 by main goroutine:
  main.main()
      redacted/main.go:29 +0x1e5

Previous read at 0x00c0000b4018 by goroutine 7:
  main.(*User).PrintAddr()
      redacted/main.go:19 +0x44

Goroutine 7 (finished) created at:
  main.main()
      redacted/main.go:30 +0x244
==================
1 (PrintAddr): 0xc0000b4018
1 (PrintAddr): 0xc0000c2138
2 (PrintAddr): 0xc0000b4018
2 (PrintAddr): 0xc0000c2150
3 (PrintAddr): 0xc0000b4018
3 (PrintAddr): 0xc0000c2168
Found 1 data race(s)

for 循环(第 29 行)不断更新循环变量 user while(即在没有适当同步的情况下以并发方式)PrintAddr 方法通过其指针接收器访问它(第 19 行)。请注意,如果您不在第 30 行将 user.PrintAddr() 作为 goroutine 启动,问题就会消失。

the Wiki you link to.

的底部实际上给出了问题和解决方案。

But why did the situation in the article not apply here and I didn't get many 3 (PrintAddr) instead?

那个同步错误是不受欢迎的不确定性的来源。特别是,您无法预测 3 (PrintAddr) 将被打印多少次(如果有的话),并且该数字可能会因一次执行而异。事实上,向上滚动并亲眼看看:在我的执行过程中,竞争检测器打开,输出恰好包含 0 到 3 之间的每个整数中的两个,尽管存在错误;但不能保证。

解决方案

只需在循环顶部隐藏循环变量 user,问题就会消失:

for i, user := range users {
    user := user // <---
    go user.PrintAddr()
    go users[i].PrintAddr()
}

PrintAddr 现在将对最里面的 user 变量进行操作,该变量不会被第 29 行的 for 循环更新。

(Playground)

附录

你还应该使用一个等待组来等待你所有的 goroutines 完成。 time.Sleep 无法协调 goroutines。