作为 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)
}
代码输出如下:
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-
那么这里一切都很好,每个条目都使用相同的地址打印了两次:
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)
呢?
代码的第一个版本超过值 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 值等于 (&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 启动,问题就会消失。
的底部实际上给出了问题和解决方案。
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
循环更新。
附录
你还应该使用一个等待组来等待你所有的 goroutines 完成。 time.Sleep
无法协调 goroutines。
我读了这个 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)
}
代码输出如下:
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-
那么这里一切都很好,每个条目都使用相同的地址打印了两次:
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)
呢?
代码的第一个版本超过值 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 值等于 (&user).PrintAddr()
:
If
x
is addressable and &x's method set containsm
,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 启动,问题就会消失。
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
循环更新。
附录
你还应该使用一个等待组来等待你所有的 goroutines 完成。 time.Sleep
无法协调 goroutines。