How do goroutines work? (or: goroutines and OS threads relation)

How can other goroutines keep executing whilst invoking a syscall? (when using GOMAXPROCS=1)
As far as I'm aware of, when invoking a syscall the thread gives up control until the syscall returns. How can Go achieve this concurrency without creating a system thread per blocking-on-syscall goroutine?

From the documentation:

Goroutines

They're called goroutines because the existing terms—threads, coroutines, processes, and so on—convey inaccurate connotations. A goroutine has a simple model: it is a function executing concurrently with other goroutines in the same address space. It is lightweight, costing little more than the allocation of stack space. And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required.

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.


Solution 1:

If a goroutine is blocking, the runtime will start a new OS thread to handle the other goroutines until the blocking one stops blocking.

Reference : https://groups.google.com/forum/#!topic/golang-nuts/2IdA34yR8gQ

Solution 2:

Ok, so here's what I've learned: When you're doing raw syscalls, Go indeed creates a thread per blocking goroutine. For example, consider the following code:

package main

import (
        "fmt"
        "syscall"
)

func block(c chan bool) {
        fmt.Println("block() enter")
        buf := make([]byte, 1024)
        _, _ = syscall.Read(0, buf) // block on doing an unbuffered read on STDIN
        fmt.Println("block() exit")
        c <- true // main() we're done
}

func main() {
        c := make(chan bool)
        for i := 0; i < 1000; i++ {
                go block(c)
        }
        for i := 0; i < 1000; i++ {
                _ = <-c
        }
}

When running it, Ubuntu 12.04 reported 1004 threads for that process.

On the other hand, when utilizing Go's HTTP server and opening 1000 sockets to it, only 4 operating system threads were created:

package main

import (
        "fmt"
        "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":8080", nil)
}

So, it's a mix between an IOLoop and a thread per blocking system call.

Solution 3:

It can't. There's only 1 goroutine that can be running at a time when GOMAXPROCS=1, whether that one goroutine is doing a system call or something else.

However, most blocking system calls, such as socket I/O, waiting for a timer are not blocked on a system call when performed from Go. They're multiplexed by the Go runtime onto epoll, kqueue or similar facilities the OS provides for multiplexing I/O.

For other kinds of blocking system calls that cannot be multiplexed with something like epoll, Go does spawn a new OS thread, regardless of the GOMAXPROCS setting (albeit that was the state in Go 1.1, I'm not sure if the situation is changed)