英文:
The variable in Goroutine not changed as expected
问题
代码如下所示:
package main
import (
"fmt"
// "sync"
"time"
)
var count = uint64(0)
//var l sync.Mutex
func add() {
for {
// l.Lock()
// fmt.Println("Start ++")
count++
// l.Unlock()
}
}
func main() {
go add()
time.Sleep(1 * time.Second)
fmt.Println("Count =", count)
}
案例:
- 在不修改代码的情况下运行,你将得到"Count = 0"。这是否符合预期?
- 只取消注释第16行的代码"fmt.Println("Start ++")",你将得到大量的"Start ++"输出以及一些带有Count值的输出,例如"Count = 11111"。这是否符合预期?
- 只取消注释第11行的代码"var l sync.Mutex",第15行的代码"l.Lock()"和第18行的代码"l.Unlock()",并将第16行的代码保持注释状态,你将得到类似"Count = 111111111"的输出。这是否符合预期?
所以...我的共享变量使用有问题吗?我的问题是:
- 为什么案例1的Count为0?
- 如果案例1是符合预期的,为什么会发生案例2?
环境:
- go version go1.8 linux/amd64
- 3.10.0-123.el7.x86_64
- CentOS Linux release 7.0.1406 (Core)
英文:
The codes are simple as below:
package main
import (
"fmt"
// "sync"
"time"
)
var count = uint64(0)
//var l sync.Mutex
func add() {
for {
// l.Lock()
// fmt.Println("Start ++")
count++
// l.Unlock()
}
}
func main() {
go add()
time.Sleep(1 * time.Second)
fmt.Println("Count =", count)
}
Cases:
- Running the code without changing, u will get "Count = 0". Not expected??
- Only uncomment line 16 "fmt.Println("Start ++")"; u will get output with lots of "Start ++" and some value with Count like "Count = 11111". Expected??
- Only uncomment line 11 "var l sync.Mutex", line 15 "l.Lock()" and line 18 "l.Unlock()" and keep line 16 commented; u will get output like "Count = 111111111". Expected.
So... something wrong with my usage in shared variable...? My question:
- Why case 1 had 0 with Count?
- If case 1 is expected, why case 2 happened?
Env:
- go version go1.8 linux/amd64
- 3.10.0-123.el7.x86_64
- CentOS Linux release 7.0.1406 (Core)
答案1
得分: 6
你在count
上有一个数据竞争。结果是未定义的。
package main
import (
"fmt"
// "sync"
"time"
)
var count = uint64(0)
//var l sync.Mutex
func add() {
for {
// l.Lock()
// fmt.Println("Start ++")
count++
// l.Unlock()
}
}
func main() {
go add()
time.Sleep(1 * time.Second)
fmt.Println("Count =", count)
}
输出:
$ go run -race racer.go
==================
WARNING: DATA RACE
Read at 0x0000005995b8 by main goroutine:
runtime.convT2E64()
/home/peter/go/src/runtime/iface.go:255 +0x0
main.main()
/home/peter/gopath/src/so/racer.go:25 +0xb9
Previous write at 0x0000005995b8 by goroutine 6:
main.add()
/home/peter/gopath/src/so/racer.go:17 +0x5c
Goroutine 6 (running) created at:
main.main()
/home/peter/gopath/src/so/racer.go:23 +0x46
==================
Count = 42104672
Found 1 data race(s)
$
参考资料:
Benign data races: what could possibly go wrong?
英文:
You have a data race on count
. The results are undefined.
package main
import (
"fmt"
// "sync"
"time"
)
var count = uint64(0)
//var l sync.Mutex
func add() {
for {
// l.Lock()
// fmt.Println("Start ++")
count++
// l.Unlock()
}
}
func main() {
go add()
time.Sleep(1 * time.Second)
fmt.Println("Count =", count)
}
Output:
$ go run -race racer.go
==================
WARNING: DATA RACE
Read at 0x0000005995b8 by main goroutine:
runtime.convT2E64()
/home/peter/go/src/runtime/iface.go:255 +0x0
main.main()
/home/peter/gopath/src/so/racer.go:25 +0xb9
Previous write at 0x0000005995b8 by goroutine 6:
main.add()
/home/peter/gopath/src/so/racer.go:17 +0x5c
Goroutine 6 (running) created at:
main.main()
/home/peter/gopath/src/so/racer.go:23 +0x46
==================
Count = 42104672
Found 1 data race(s)
$
References:
答案2
得分: 1
没有任何同步,你就没有任何保证。
在你的第一个情况中,出现“Count = 0”的原因可能有多个:
-
你有一个多处理器或多核系统,其中一个单元(CPU或核心)正在快乐地执行for循环,而另一个单元则休眠一秒钟,然后打印你看到的那行代码。编译器完全可以生成机器代码,将值加载到某个寄存器中,并且在for循环中只增加该寄存器的值。当函数完成对变量的操作时,内存位置可以更新。在无限for循环的情况下,这永远不会发生。因为你作为程序员告诉编译器,该变量没有争用,所以省略了任何同步。
在使用互斥锁的版本中,同步原语告诉编译器,可能有其他线程获取了互斥锁,因此在解锁互斥锁之前,它需要将寄存器中的值写回内存位置。至少可以这样考虑。实际发生的情况是,解锁和稍后的锁操作在两个goroutine之间引入了happens before关系,并且这提供了一个保证:我们将在另一个线程中的锁操作之后看到在解锁之前对变量的所有写入,如go内存模型锁中所述,无论如何实现。
-
Go运行时调度器根本不运行for循环,直到主goroutine中的休眠完成。(这不太可能,但如果我记得正确的话,没有保证这不会发生。)遗憾的是,关于Go调度器如何工作的官方文档很少,但它只能在某些点上调度goroutine,它不是真正的抢占式。这样做的后果是严重的。例如,你可以通过启动与核心数相同数量的goroutine,在无限的for循环中只增加一个变量,使你的程序永远运行下去。主goroutine没有剩余的核心(可以结束程序),而调度器无法抢占一个只执行简单操作(如增加一个变量)的无限for循环的goroutine。我不知道现在是否有所改变。
-
正如其他人指出的,这是数据竞争,请搜索并了解相关知识。
你的两个版本之间的区别只是在第16行的注释/取消注释,这可能只是因为运行时间的原因,因为向终端打印输出可能会相当慢。
对于一个正确的程序,在主程序的休眠之后,你还需要在fmt.Println之前锁定互斥锁,并在之后解锁它。但是,对于输出结果,不能有确定性的期望,因为结果会随着机器/操作系统的不同而变化...
英文:
Without any synchronisation you have no guarantees at all.
There could be multiple reasons, why you see 'Count = 0' in your first case:
-
You have a multi processor or multi core system and one unit (cpu or core) is happily churning away at the for loop, while the other sleeps for one second and prints the line you are seeing afterwards. It would be completely legal for the compiler to generate machine code, which loads the value into some register and only ever increase that register in the for loop. The memory location can be updated, when the function is done with the variable. In case of an infinite for loop, that ist never. As you, the programmer told the compiler, that there is no contention about that variable, by omitting any synchronisation.
In your mutex version the synchronisation primitives tell the compiler,
that there might be some other thread taking the mutex, so it needs to write back the value from the register to the memory location before unlocking the mutex. At least one can think about it like that. What really happens that the unlock and a later lock operation introduce a happens before relation between the two go routines and this gives the guarantee, that we will see all writes to variables from before the unlock in one thread after the lock operation in the other thread, as described in go memory model locks howsoever this is implemented. -
The Go runtime scheduler doesn't run the for loop at all, until the sleep in the main go routine is done. (Isn't likely, but, if I recall correctly, there is not guarantee that this isn't happening.) Sadly there is not much official documentation available about how the scheduler works in go, but it can only schedule a goroutine at certain points, it is not really preemptive. The consequences of this are severe. For example you could make your program run forever in some versions of go, by firing up as many go routines, as you had cores, doing endless for loops only incrementing a variable. There was no core left for the main go routine (which could end the program) and the scheduler can't preempt a go routine in an endless for loop doing only simple stuff, like incrementing a variable. I don't know, if that is changed now.
-
as others pointed out, that is a data race, google it and read up about it.
The difference between your versions there only line 16 is commented/uncommented is likely only because of run time, as printing to a terminal can be pretty slow.
For a correct program, you need to additionally lock the mutex after your sleep in you main program and before the fmt.Println and unlock it afterwards. But there can't be a deterministic expectation about the output, as the result will vary with machine/os/...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论