How Go runtime checks if goroutine is blocked?

Viewed 2938

Go docs says that:

When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked

But how does runtime detect that goroutine is blocked?

For example if I will run calculation in one of go-routine will it be evaluated as blocking operation?

package main

import (
    "fmt"
    "runtime"
)

func f(from string, score int) {
    for i := 0; i < score; i++ {
            for z := 0; z < score; z++ {
        }
    }

    fmt.Println(from, " Done")
}   

func main() {
runtime.GOMAXPROCS(1)
f("direct", 300000)
go f("foo", 200000)
go f("bar", 20000)
go f("baz", 2000)

 go func(msg string) {
        fmt.Println(msg)
    }("going without loop")

       var input string
    fmt.Scanln(&input)
    fmt.Println("done")
}

I am getting result: baz, boo bar. But why? Does Go understand that foo is blocking?

3 Answers

This ticket is relevant to the questions:

https://github.com/golang/go/issues/11462

Each blocking call you could do, will be served by the runtime. So the runtime knows if something happens which could block.

For example:

  1. If you call Lock() on a sync.Mutex the runtime will handle that and checks wether that would block or not and act accordingly.

  2. If you call Run() or Output() (or the like) on a exec.Cmd the runtime notices that and assumes, that this call will block. It can't know wether the program you are running will block, so it has to assume the worst.

To the best of my knowledge, there are two principle mechanisms, how a goroutine could block and the examples above are examples for each one.

  1. Is an example for things the runtime provides to you without "external" help.

  2. Is an example for things were a syscall is involved. Golang on linux for example doesn't use gnu libc and implements the syscalls it needs directly by calling the os. All those calls are going through package syscall (to the best of my knowledge) and here the runtime has a single hook to get notified of what happens.

Of course the picture is a bit muddy as it might be, that golang needs a mutex from the os for cross os thread synchronization even for 1. and then it is also somehow a bit of example 2.

Regarding the code in the question: No. Go doesn't understand that f might take a lot of time, if the loops aren't optimised away by the compiler. And in such a tight loop, the go scheduler can't "stop" the goroutine and set another as running, as there is no preemption point in the loop. So if you have mulitple of such goroutines doing tight loops without preemption points, they might eat up your cpus and all other goroutines have to wait, until at least one of the tight loops is done. But only calling a different function in that loop changes that picture, as a function call is a preemption point.

user1432751 asked in a comment:

what happens with CPU registers when blocking operation is happen? Current thread is blocking by goroutines, Go scheduler create new system thread and migrate all other threads there? –

The go schedule isn't preemptive (at least that was the state last time I checked) but schedules at certain preemption points. For example syscalls, sends and waits on channels and if I remember correctly function calls. So at those points an internal function is called and the cpu registers relevent to the code executing in the goroutine are already on the stack, when the scheduler decides what to do.

And yes, if a syscall is done and therefore there is the danger of a blocked os thread, the goroutine doing the syscall gets its own os thread, that doesn't even count to GOMAXPROCS.

Blocking in this context usually happens when a goroutine has asked the runtime to do work, usually via a system call. Listening on a socket, for instance — the goroutine tells the kqueue/epoll part of the system to wake it up when data arrives, at which point it is very obviously blocked.

Goroutines are also blocked when waiting for a message on a channel - here the goroutine has explicitly told the runtime that it's not going to be doing anything else until it receives a message, so the runtime knows very clearly that the goroutine is blocked.

Most operations that of the form "do this and give me a result" or "give me this when it's ready" are all verifiably blocking operations, and are put to sleep until ready.

I had the same question few months ago and I solved it adding a channel that should be updated in less of certain time, like a watchdog:

out := make(chan bool)      
go func() {             
    for {
        // Do some stuff here
        time.Sleep(10 * time.Millisecond) // leaves CPU free for 10ms
        out <- true             
    }
}()     
// main loop
for { 
    select {            
        case <-out: // Jump here when out channel is filled             
        case <-time.After(10 * time.Second): // Jump here if there is no channel activity for 10s
    }       
}

I hope it would be useful for you.

Related