3

Going through Go by Example: Atomic Counters. The code example calls runtime.Gosched after calling atomic.AddUint64.

atomic.AddUint64 is called to

ensure that this goroutine doesn’t starve the scheduler

Unfortunately, I am finding the explanation not so meaty and satisfying.

I tried running the sample code (comments removed for conciseness):

package main

import "fmt"
import "time"
import "sync/atomic"
import "runtime"

func main() {

    var ops uint64 = 0

    for i := 0; i < 50; i++ {
        go func() {
            for {
                atomic.AddUint64(&ops, 1)

                runtime.Gosched()
            }
        }()
    }

    time.Sleep(time.Second)

    opsFinal := atomic.LoadUint64(&ops)
    fmt.Println("ops:", opsFinal)
}

without the runtime.Gosched() (go run conc.go) and the program never exited even when I reduced the loop from 50 to 1.

Question:

What happens under the hood after the call to atomic.AddUint64 that it is necessary to call runtime.Gosched? And how does runtime.Gosched fixes this? I did not find any hint to such a thing in sync/atomic's documentation.

Bleeding Fingers
  • 6,993
  • 7
  • 46
  • 74
  • Maybe the best advice would be: Use neither `sync/atomic` nor `runtime.Gosched`. The atomic operations in `snyc/atomic` are there because **sometimes** atomic operations provide significant performance gain. Most user will not need these and most should not use atomic operations as this quickly becomes complicated. Keep away from such low-level stuff. – Volker Oct 19 '15 at 09:23
  • 4
    The call to `Gosched` has _abolutely_ _nothing_ to do with `atomic.AddUint64`. You could substitute `i++` for the AddUint64. It just happens, that the inner loop is too tight and won't yield to the scheduler, but this has nothing to do with atomic operations. See David's answer for details. – Volker Oct 19 '15 at 09:27
  • @Volker, I'm afraid, `i++` isn't a preemption point. Back in the day, they happened to be at syscalls and calls to `C` but since some version (1.4 ?) normal Go function calls also serve as preemption points, so something like `func(){return}()` should suffice (well, unless may be such a call could be completely optimized away). – kostix Oct 19 '15 at 12:31
  • 1
    @kostix Sorry, I was unclear: You could substitute `i++` for `AddUint64` (not for `GoSched`). This would be racy, but as you stated, it would not allow preemption. – Volker Oct 19 '15 at 12:51

1 Answers1

5

This is how cooperative multithreading works. If one thread remains ready to run, it continues to run and other threads don't. Explicit and implicit pre-emption points are used to allow other threads to run. If your thread has a loop that it stays in for lots of time with no implicit pre-emption points, you'll starve other threads if you don't add an explicit pre-emption point.

This answer has much more information about when Go uses cooperative multithreading.

Community
  • 1
  • 1
David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • The answer you mention is correct but as of Go 1.5, GOMAXPROCS is automatically set to how many CPU cores you have. – wldsvc Oct 19 '15 at 10:40
  • 6
    @wldsvc That doesn't change anything. You still have cooperative multithreading. If, for example, GOMAXPROCS is 8 and you have 9 threads, none of which hit any pre-emption points, one of the threads will still be starved. It's not about how many threads you have but about how you switch what tasks the threads are doing. (Also, there are still machines with only one core. Virtual machines with one core aren't even uncommon.) – David Schwartz Oct 19 '15 at 10:46