Search code examples
gogoroutinego-scheduler

Why isn't this goroutine run, even with a `time.Sleep`?


Take this piece of code:

func main() {
    var x int
    go func() {
        for {
            x++
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

Why does x equal 0 at the end? I understand that Go's scheduler needs the time.Sleep() call to pickup the goroutine but why it isn't doing so?

Hint: Putting a time.Sleep() or a call to runtime.Gosched() inside the for loop fixes this code. But why?

Update: Check the following version of the same code:

func main() {
    var x int
    go func() {
        for i := 0; i < 10000; i++ {
            x++
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

Curiously the code inside the goroutine now is executed and x is no longer 0. Does the compiler do any optimization here?


Solution

  • It's important to know what you're asking here. There is no promise in Go that this program will do anything in particular, because the program is invalid. But as an exploration of the optimizer, it can be interesting to give some insight on how it is currently implemented. Any program that relied on this information would be very fragile and invalid, but still it's a curiosity.

    We can compile the program, and then look at the output. I particularly like the two versions you've given, because they let use see the differences. I've done my decompilation using Hopper (these are compiled with go1.14 darwin/amd64).

    In the second case, the goroutine looks like you'd think it would:

    void _main.main.func1(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6) {
        rax = arg6;
        for (rcx = 0x0; rcx < 0x2710; rcx = rcx + 0x1) {
                *rax = *rax + 0x1;
        }
        return;
    }
    

    Nothing too surprising here. But what about the first case that you're curious about:

    _main.main.func1:
        goto _main.main.func1;
    

    It becomes a noop. Quite literally; here's the assembly:

                         _main.main.func1:
    000000000109d1b0         nop                                                    ; CODE XREF=_main.main.func1+1
    000000000109d1b1         jmp        _main.main.func1                            ; _main.main.func1
    

    How does this happen? Well, the compiler can look at this code:

    go func() {
        for {
            x++
        }
    }()
    

    And it knows that nothing ever reads x. There's no way anything ever could read x, because there is no locking around x and this goroutine never terminates. So there's nothing that can ever read x after this goroutine completes. See The Go Memory Model for more about what it means for something to happen before something else or after something else.

    "But I do read x!" No you don't. That would be invalid code and the compiler knows you didn't write invalid code. Who would do that when there's a race detector telling you this is invalid? So since the compiler can clearly see that nothing ever reads x, there is no reason to bother updating it.

    In your limited-loop example, the goroutine terminates, so it could be possible that something reads x after that. The compiler isn't smart enough to notice that no valid read is ever made and so it doesn't optimize this as well as it could. Maybe a future compiler will be smart enough to output 0 in both cases. And maybe a future compiler will be smart enough to completely delete your no-op goroutine in the first case.

    But the key point here is that the infinite-loop case is completely correct, though slightly less efficient than it could be. And the non-infinite-loop case is also completely correct, though quite a lot less efficient as it could be.