Case study of an atypical unlocked read and write variable in Go


daniel

Some time ago, I saw a problem in V2 about concurrent reading and writing variables: go one thread writes, another thread reads, why can’t guarantee the final consistency? The example given in the post is very simple (with some modifications) main.go:

package main

import (
    "fmt"
    "runtime"
    "time"
)

var i = 0

func main() {
    runtime.GOMAXPROCS(2)
    go func() {
        for {
            fmt.Println("i am here", i)
            time.Sleep(time.Second)
        }
    }()
    for {
        i += 1
    }
}
Copy the code

Since it is a problem post, the results of direct operation should be unexpected to most people:

╰─ Go Run main. Go 1 Address I am here 0 I am here 0 I am here 0 I am here 0 I am here 0 I am here 0 I am here 0 I am here 0 I am here 0 I am here 0 I am here 0 I am here 0...Copy the code

The reply of the post is more, the amount of information involved is relatively messy, but after climbing the floor, I feel that I don’t understand. The reason for the above result is that the go compiler optimizes the code I increment for loop by 1. To verify this, we use go tool objdump -s ‘main\.main’ main to look at the compiled binary executable’s assembly code:

╰ ─ ➤ go tool objdump -s' main \. Main 'main TEXT main. The main (SB)/Users/liudanking/code/golang gopath/SRC/test/main. Go main.go:11 0x108de60 65488b0c25a0080000 MOVQ GS:0x8a0, CX main.go:11 0x108de69 483b6110 CMPQ 0x10(CX), SP main.go:11 0x108de6d 7635 JBE 0x108dea4 main.go:11 0x108de6f 4883ec18 SUBQ $0x18, SP main.go:11 0x108de73 48896c2410 MOVQ BP, 0x10(SP) main.go:11 0x108de78 488d6c2410 LEAQ 0x10(SP), BP main.go:12 0x108de7d 48c7042402000000 MOVQ $0x2, 0(SP) main.go:12 0x108de85 e8366bf7ff CALL runtime.GOMAXPROCS(SB) main.go:13 0x108de8a c7042400000000 MOVL $0x0, 0(SP) main.go:13 0x108de91 488d05187f0300 LEAQ go.func.*+115(SB), AX main.go:13 0x108de98 4889442408 MOVQ AX, 0x8(SP) main.go:13 0x108de9d e8fe13faff CALL runtime.newproc(SB) main.go:20 0x108dea2 ebfe JMP 0x108dea2 main.go:11 0x108dea4 e8c7dffbff CALL runtime.morestack_noctxt(SB) main.go:11 0x108dea9 ebb5 JMP main.main(SB) :-1 0x108deab cc INT $0x3 :-1 0x108deac cc INT $0x3 :-1 0x108dead cc INT $0x3 :-1 0x108deae cc INT $0x3 :-1 0x108deaf cc INT $0x3 TEXT main.main.func1(SB) /Users/liudanking/code/golang/gopath/src/test/main.go main.go:13 0x108deb0 65488b0c25a0080000 MOVQ GS:0x8a0, CX main.go:13 0x108deb9 483b6110 CMPQ 0x10(CX), SP main.go:13 0x108debd 0f8695000000 JBE 0x108df58 main.go:13 0x108dec3 4883ec58 SUBQ $0x58, SP main.go:13 0x108dec7 48896c2450 MOVQ BP, 0x50(SP) main.go:13 0x108decc 488d6c2450 LEAQ 0x50(SP), BP main.go:15 0x108ded1 0f57c0 XORPS X0, X0 main.go:15 0x108ded4 0f11442430 MOVUPS X0, 0x30(SP) main.go:15 0x108ded9 0f11442440 MOVUPS X0, 0x40(SP) main.go:15 0x108dede 488d059b020100 LEAQ runtime.types+65664(SB), AX main.go:15 0x108dee5 4889442430 MOVQ AX, 0x30(SP) main.go:15 0x108deea 488d0d0f2d0400 LEAQ main.statictmp_0(SB), CX main.go:15 0x108def1 48894c2438 MOVQ CX, 0x38(SP) main.go:15 0x108def6 488d1583fb0000 LEAQ runtime.types+63872(SB), DX main.go:15 0x108defd 48891424 MOVQ DX, 0(SP) main.go:15 0x108df01 488d1d107c0c00 LEAQ main.i(SB), BX main.go:15 0x108df08 48895c2408 MOVQ BX, 0x8(SP) main.go:15 0x108df0d e84eddf7ff CALL runtime.convT2E64(SB) main.go:15 0x108df12 488b442410 MOVQ 0x10(SP), AX main.go:15 0x108df17 488b4c2418 MOVQ 0x18(SP), CX main.go:15 0x108df1c 4889442440 MOVQ AX, 0x40(SP) main.go:15 0x108df21 48894c2448 MOVQ CX, 0x48(SP) main.go:15 0x108df26 488d442430 LEAQ 0x30(SP), AX main.go:15 0x108df2b 48890424 MOVQ AX, 0(SP) main.go:15 0x108df2f 48c744240802000000 MOVQ $0x2, 0x8(SP) main.go:15 0x108df38 48c744241002000000 MOVQ $0x2, 0x10(SP) main.go:15 0x108df41 e85a9dffff CALL fmt.Println(SB) main.go:16 0x108df46 48c7042400ca9a3b MOVQ $0x3b9aca00, 0(SP) main.go:16 0x108df4e e87d27fbff CALL time.Sleep(SB) main.go:15 0x108df53 e979ffffff JMP 0x108ded1 main.go:13 0x108df58 e813dffbff CALL runtime.morestack_noctxt(SB) main.go:13 0x108df5d e94effffff JMP main.main.func1(SB) :-1 0x108df62 cc INT $0x3 :-1 0x108df63 cc INT $0x3 :-1 0x108df64 cc INT $0x3 :-1 0x108df65 cc INT $0x3 :-1 0x108df66 cc INT  $0x3 :-1 0x108df67 cc INT $0x3 :-1 0x108df68 cc INT $0x3 :-1 0x108df69 cc INT $0x3 :-1 0x108df6a cc INT $0x3 :-1 0x108df6b cc INT $0x3 :-1 0x108df6c cc INT $0x3 :-1 0x108df6d cc INT $0x3 :-1 0x108df6e cc INT $0x3 :-1 0x108df6f cc INT  $0x3Copy the code

Obviously,

    for {
        i += 1
    }
Copy the code

It’s optimized out of the picture. We can add another statement to statement I += 1 to avoid being optimized out:

    for {
        i += 1
        time.Sleep(time.Nanosecond)
    }
Copy the code

Rerun the program and the result “looks correct” :

╰─ Go Run Main. Go 1 Mins I am here 30 I am here 1806937 I am here 3853635 I am here 5485251...Copy the code

Obviously, after this modification, the code is not really correct. Because variable I has the problem of concurrent reads and writes, the data race. However, in the data Race scenario, the behavior of GO is unknown. Uncertainty is one of the few things programmers hate the most. As a result, writing out data Race bugs one step ata time is not fun to debug. In this example, because there are only a few lines of code, we can visually locate the problem. If the code is large, we can use the -race parameter in the Golang tool chain to troubleshoot this type of problem:

╰─ Go 2 Mins ================== WARNING: DATA Race Read at 0x0000011D4318 by Goroutine 6: runtime.convT2E64() /usr/local/go/src/runtime/iface.go:335 +0x0 main.main.func1() /Users/liudanking/code/golang/gopath/src/test/main.go:15 +0x7d Previous write at 0x0000011d4318 by main goroutine: main.main() /Users/liudanking/code/golang/gopath/src/test/main.go:20 +0x7f Goroutine 6 (running) created at: main.main() /Users/liudanking/code/golang/gopath/src/test/main.go:13 +0x53 ================== i am here 1 i am here 558324 i am here 1075838Copy the code

In addition to using -trace on go Run, several other commonly used Golang toolchain directives also support this parameter:

$ go test -race mypkg // to test the package $ go run -race mysrc.go // to run the source file $ go build -race mycmd //  to build the command $ go install -race mypkg // to install the packageCopy the code

It is important to note that -trace does not guarantee that all data races in the program will be detected, and that data races must exist. It’s a little tricky, but remember that it’s the same truth table as the Bloom Filter.

There are many ways to correct The code mentioned above. We can use The sync package recommended by The Go Memory Model:

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

var i = 0

func main() {
    runtime.GOMAXPROCS(2)
    mtx := sync.RWMutex{}
    go func() {
        for {
            mtx.RLock()
            fmt.Println("i am here", i)
            mtx.RUnlock()
            time.Sleep(time.Second)
        }
    }()
    for {
        mtx.Lock()
        i += 1
        mtx.Unlock()
        time.Sleep(time.Nanosecond)
    }
Copy the code

Further reading

  • Data Race Detector
  • The Go Memory Model
  • The role of volatile in C
  • The memory barrier