0

The accepted answer at golang methods that will yield goroutines explains that Go's scheduler will yield control from one goroutine to another when a syscall is encountered. I understand that this means if you have multiple goroutines running, and one begins to wait for something like an HTTP response, the scheduler can use this as a hint to yield control from that goroutine to another.

But what about situations where there are no syscalls involved? What if, for example, you had as many goroutines running as logical CPU cores/threads available, and each were in the middle of a CPU-intensive calculation that involved no syscalls. In theory, this would saturate the CPU's ability to do work. Would the Go scheduler still be able to detect an opportunity to yield control from one of these goroutines to another, that perhaps wouldn't take as long to run, and then return control back to one of these goroutines performing the long CPU-intensive calculation?

Matt Welke
  • 1,441
  • 1
  • 15
  • 40
  • 3
    More recent versions (since 1.14 IIRC) can preempt goroutines, meaning a goroutine can still yield even if it doesn't issue any syscalls. – Burak Serdar Aug 08 '21 at 01:05
  • yeap, this is the answer. – Oleg Butuzov Aug 08 '21 at 01:11
  • 1
    There is not "the one Go scheduler". This thing evolves. – Volker Aug 08 '21 at 05:13
  • "can still yield even if it doesn't issue any syscalls" Not to be too pedantic here, but I'm curious, is it that they *can* yield (and the programmer must do something in particular to make that happen) or that they automatically *will* yield? – Matt Welke Aug 08 '21 at 05:26

2 Answers2

2

There are few if any promises here.

The Go 1.14 release notes says this in the Runtime section:

Goroutines are now asynchronously preemptible. As a result, loops without function calls no longer potentially deadlock the scheduler or significantly delay garbage collection. This is supported on all platforms except windows/arm, darwin/arm, js/wasm, and plan9/*.

A consequence of the implementation of preemption is that on Unix systems, including Linux and macOS systems, programs built with Go 1.14 will receive more signals than programs built with earlier releases. This means that programs that use packages like syscall or golang.org/x/sys/unix will see more slow system calls fail with EINTR errors. ...

I quoted part of the third paragraph here because this gives us a big clue as to how this asynchronous preemption works: the runtime system has the OS deliver some OS signal (SIGALRM, SIGVTALRM, etc.) on some sort of schedule (real or virtual time). This allows the Go runtime to implement the same kind of schedulers that real OSes implement with real (hardware) or virtual (virtualized hardware) timers. As with OS schedulers, it's up to the runtime to decide what to do with the clock ticks: perhaps just run the GC code, for instance.

We also see a list of platforms that don't do it. So we probably should not assume it will happen at all.

Fortunately, the runtime source is actually available: we can go look to see what does happen, should any given platform implement it. This shows that in runtime/signal_unix.go:

// We use SIGURG because it meets all of these criteria, is extremely
// unlikely to be used by an application for its "real" meaning (both
// because out-of-band data is basically unused and because SIGURG
// doesn't report which socket has the condition, making it pretty
// useless), and even if it is, the application has to be ready for
// spurious SIGURG. SIGIO wouldn't be a bad choice either, but is more
// likely to be used for real.
const sigPreempt = _SIGURG

and:

// doSigPreempt handles a preemption signal on gp.
func doSigPreempt(gp *g, ctxt *sigctxt) {
        // Check if this G wants to be preempted and is safe to
        // preempt.
        if wantAsyncPreempt(gp) && isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()) {
                // Inject a call to asyncPreempt.
                ctxt.pushCall(funcPC(asyncPreempt))
        }

        // Acknowledge the preemption.
        atomic.Xadd(&gp.m.preemptGen, 1)
        atomic.Store(&gp.m.signalPending, 0)
}

The actual asyncPreempt function is in assembly, but it just does some assembly-only trickery to save user registers, and then calls asyncPreempt2 which is in runtime/preempt.go:

//go:nosplit
func asyncPreempt2() {
        gp := getg()
        gp.asyncSafePoint = true
        if gp.preemptStop {
                mcall(preemptPark)
        } else {
                mcall(gopreempt_m)
        }
        gp.asyncSafePoint = false
}

Compare this to runtime/proc.go's Gosched function (documented as the way to voluntarily yield):

//go:nosplit

// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
        checkTimeouts()
        mcall(gosched_m)
}

We see the main differences include some "async safe point" stuff and that we arrange for an M-stack-call to gopreempt_m instead of gosched_m. So, apart from the safety check stuff and a different trace call (not shown here) the involuntary preemption is almost exactly the same as voluntary preemption.

To find this, we had to dig rather deep into the (Go 1.14, in this case) implementation. One might not want to depend too much on this.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Nice, thanks for digging up all that info. It sounds to me like this is definitely not something one would want to rely on. They should code their functions expecting that the Go runtime will block on them until the function is complete, unless they're willing to use `runtime.Gosched` to split their work up. – Matt Welke Aug 08 '21 at 17:00
0

A little bit more on this to complete @torek's answer. Goroutines are interruptible when there is a syscall, but also when a routine is waiting on a lock, a chan or sleeping.

As @torek's said, since 1.14 routines can also be preempted even when they do none of the above. The scheduler can mark any routine as preemptible after it ran for more than 10ms.

More reading there: https://medium.com/a-journey-with-go/go-goroutine-and-preemption-d6bc2aa2f4b7

jeremie
  • 971
  • 9
  • 19