【问题标题】:Unexpected behavior from launching a method call on a loop variable as a goroutine作为 goroutine 在循环变量上启动方法调用时的意外行为
【发布时间】:2021-07-18 11:05:42
【问题描述】:

我读到了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

我也不明白,为什么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)

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

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)呢?

【问题讨论】:

  • "我也搞不懂,为什么3 (PrintAddr)中有4个是0xc00000c030,而最后一个不一样?" -- 的4个实例3 (PrintAddr): 0xc00000c030user.PrintAddr() 的结果。 3 (PrintAddr): 0xc0000561c8 实例和 1,2,0 (PrintAddr) 0x... 实例是 users[i].PrintAddr() 的结果。

标签: loops go pointers goroutine data-race


【解决方案1】:

问题

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

$ 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 行)不断更新循环变量 userwhile(即以没有适当同步的并发方式)PrintAddr 方法通过其指针接收器访问它(行19)。请注意,如果您不在第 30 行将 user.PrintAddr() 作为 goroutine 启动,问题就会消失。

the Wiki you link to底部其实给出了问题和解决方法。

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

同步错误是不受欢迎的不确定性的来源。特别是,您无法预测将打印多少次(如果有)3 (PrintAddr),并且该数字可能会因一次执行而异。实际上,向上滚动并亲自查看:在我打开竞赛检测器的执行过程中,输出碰巧具有两个介于 0 和 3 之间的整数,尽管存在错误;但不能保证。

解决方案

只需在循环顶部添加阴影循环变量user,问题就消失了:

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

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

(Playground)

附录

您还应该使用等待组来等待所有 goroutine 完成。 time.Sleep 是无法协调 goroutines 的。

【讨论】:

  • 说实话,我不是在寻找解决方案,而是在寻找解释,为什么会发生这种情况
【解决方案2】:

slice 值范围内的第一个代码版本是taking 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方法上的值等于(&amp;user).PrintAddr()

如果x 是可寻址的并且&x 的方法集包含m,则x.m()(&amp;x).m() 的简写

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

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

【讨论】:

  • The first version of your code that ranges over the value slice is taking the address of the iterator variable..是的,我重复了文章中的错误并尝试尝试一下
  • 我这样做是为了更好地了解发生了什么
  • Indexing the slice instead works as expected because you are accessing the actual i-th value in the slice, instead of using the iterator var. 但是 1. 它没有按预期工作(go users[i].PrintAddr() 至少打印了两次相同的地址) 2. i 是在每个循环中重复使用的相同变量,类似到user,对吧?所以它有同样的“问题”,不是吗?
  • Changing the slice to hold pointer values also works around this issue because the iterator var is now a copy of the pointer to the User value. 是的,当然,但在我的理解中,它仍然是一个变量,只是随着每次迭代而改变,所以应该有相同的“问题”,但是,由于某种原因,没有...
  • @Wynell i 没有同样的问题,因为您没有解决它。当切片包含 *User 值时,迭代器 var 确实也被重用了,但是当您在其上调用方法时,没有隐式寻址。重点是:不要处理迭代器变量。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多