Description
sync/atomic: go1.8 regression: atomic.AddUint64 is unreliable
Sometimes atomic.AddUint64
has no effect.
Update: @cespare has discovered that the loop containing atomic.AddUint64
has been elided.
Update: @rjeczalik has reported that this behavior also occurs with atomic.StoreUint64
and atomic.CompareAndSwapUint64
functions.
package main
import (
"fmt"
"runtime"
"sync/atomic"
"time"
)
var a uint64 = 0
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(runtime.NumCPU(), runtime.GOMAXPROCS(0))
go func() {
for {
atomic.AddUint64(&a, uint64(1))
}
}()
for {
val := atomic.LoadUint64(&a)
fmt.Println(val)
time.Sleep(time.Second)
}
}
For go1.4, go1.5, go1.6, and go1.7:
The atomic.AddUint64(&a, uint64(1))
statement works as expected.
$ go version
go version go1.4-bootstrap-20161024 linux/amd64
go version go1.5.4 linux/amd64
go version go1.6.4 linux/amd64
go version go1.7.5 linux/amd64
$ go build atomic.go && ./atomic
4 4
0
96231831
192599210
289043510
385369439
481772231
578143106
674509741
770966820
867408361
963866833
1060299901
<SNIP>
^C
For go1.8 and go tip:
The atomic.AddUint64(&a, uint64(1))
statement appears to have no effect.
go version go1.8 linux/amd64
go version devel +1e69aef Sat Feb 18 19:01:08 2017 +0000 linux/amd64
go version devel +1e69aef Sat Feb 18 19:01:08 2017 +0000 windows/amd64
$ uname -a
Linux peter 4.8.0-36-generic #36~16.04.1-Ubuntu SMP Sun Feb 5 09:39:57 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/peter/gopath"
GORACE=""
GOROOT="/home/peter/go"
GOTOOLDIR="/home/peter/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build842347949=/tmp/go-build -gno-record-gcc-switches"
CXX="g++"
CGO_ENABLED="0"
PKG_CONFIG="pkg-config"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
$ go build atomic.go && ./atomic
4 4
0
0
0
0
0
0
0
0
0
<SNIP>
^C
Interestingly, we can make go1.8 and go tip work with a simple code modification that should not have any effect:
package main
import (
"fmt"
"runtime"
"sync/atomic"
"time"
)
var a uint64 = 0
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(runtime.NumCPU(), runtime.GOMAXPROCS(0))
go func() {
for {
new := atomic.AddUint64(&a, uint64(1))
if new == 0 {
runtime.Gosched() // or print()
}
}
}()
for {
val := atomic.LoadUint64(&a)
fmt.Println(val)
time.Sleep(time.Second)
}
}
$ go version
go version go1.8 linux/amd64
$ go build atomic_pass.go && ./atomic_pass
4 4
0
126492073
253613883
378888824
506065798
633247293
760383560
887553077
1014723419
<SNIP>
^C
After AddUint64(&a, uint64(1)
, new
should be greater than zero and runtime.Gosched()
should not be executed. Substituting print()
for runtime.Gosched()
also works. If the line runtime.Gosched() // or print()
is commented out the program reverts to failure.