What are goroutines and how are they scheduled?

mattjamesboyle

Matt Boyle

Posted on April 4, 2022

What are goroutines and how are they scheduled?

I use goroutines all the time, but I got asked about how they are scheduled recently and I did not know the answer to a sufficient depth. I find writing things down is a great way to ensure I understand them and thought I would turn my notes into a blog in an effort to help others!

If you like this blog post and want to support me to write more whilst learning more about Go, you can check out my site bytesizego.com

What is a goroutine?

Makes sense to start out answering this.

Goroutines are the Go programming language's way of dealing with concurrency (allowing multiple things to happen at the same time). It's worth calling out the difference between concurrency and parallelism as it is an important thing to distinguish. I really like the the take Riteek Srivastav took in his blog post here, so I have copied the paragraph below wholesale. His blog post is an excellent reference on goroutines, so if you want to go a bit deeper than I did here, please check it out:

Here’s an example: I’m writing this article and feel
thirsty, I’ll stop typing and drink water. Then start
typing again. Now, I’m dealing with two jobs (typing and
drinking water) by some time slice, which is said to be
concurrent jobs. Point to be noted here is that these two tasks (writing and drinking) are not being done at the same time.

When things are being done at the same time it’s called
parallelism (Think checking your mobile AND eating chips).
So concurrency is dealing with multiple things at once
(does not need to be done at the same time) with some time schedule and parallelism(doing multiple things at the same time) is a subset of this.

goroutines are cheap to create and very easy to use. Any function in go can be run concurrently by simply appending the go keyword to the function call.

A really naive use might look as follows:

Without goroutines

package main

import (
    "context"
    "time"
)

// cancel is called as functions execute sequentially and take 4 seconds each.
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()
    someLongRunningTaskOne(ctx)
    someLongRunningTaskTwo(ctx)
}
Enter fullscreen mode Exit fullscreen mode

With goroutines:

package main

import (
    "context"
    "time"
)

// The program executes the two functions concurrency.  
// We use a sleep to stop or programming terminating 
// prematurely. More on this below.
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
    defer cancel()
    go someLongRunningTaskOne(ctx)
    go someLongRunningTaskTwo(ctx)

    time.Sleep(time.Second * 5)

}
Enter fullscreen mode Exit fullscreen mode

So goroutines are threads?

To people new to Go, the word goroutine and thread get used a little interchangeably. This makes sense if you come from a language such as Java where you can quite literally make new OS threads. Go is different, and a goroutine is not the same as a thread. Threads are much more expensive to create, use more memory and switching between threads takes longer.

Goroutines are an abstraction over threads and a single Operating System thread can run many goroutines.

So How are goroutines scheduled?

Firstly, we always have a main goroutine. This is the programs main "thread" (see, it's hard to avoid this word when discussing them!). When this main goroutine terminates, our program is complete. You always need to keep this in mind as if we do not account for this in our program, you may see some unexpected behaviour. If you look at the example of using goroutines above again, the program will not wait for either of the goroutines to complete before terminating the program. This is why we have to use a time.Sleep() but this means our program has a race condition.

From this main goroutine, we can create as many goroutines as we like, and within those goroutines, we can create goroutines. The below is totally valid go:

package main

import (
    "log"
    "time"
)

func main() {
    go func() {
        go func() {
            go func() {
                go func() {
                    go func() {
                        go func() {
                            go func() {
                                log.Println("maybe I should have thought about this program a little more")
                            }()
                        }()
                    }()
                }()
            }()
        }()
    }()
    time.Sleep(time.Second)
}
Enter fullscreen mode Exit fullscreen mode

Are goroutines called in the order I declared them?

No.

Most Operating Systems have something called a preemptive scheduler. This means that which thread is executed next is determined by the OS itself based on thread priority and other things like waiting to receive data over the network. Since goroutines are abstractions over threads, they all have the same priority and we therefore cannot control the order in which they run.

There has been discussions as far back as 2016 (you can read one such discussion here) about adding the ability to set priority on individual goroutines, but there is some pretty compelling points raised as to why its not a good idea.

How do I ensure my program is as performant as possible?

There is an environment variable (GOMAXPROCS) that you can set which determines how many threads your go program will use simultaneously. You can use this great library from Uber to automatically set the GOMAXPROCS variable to match a Linux container CPU quota. If you are running Go workloads in Kubernetes, you should use this.

If you set GOMAXPROCS to be 3, this means that the program will only execute code on 3 operating system threads at once, even if there are 1000s of goroutines.

It begs the question though, does setting GOMAXPROCS to the biggest value possible mean your program will be faster?

The answer is no, and it actually might make it slower. There are a few reasons for this, but the main reason is to do with context switching.

Swapping between threads is a relatively slow operation, and can take up to 1000ns as oppose to switching between goroutines on the same thread which takes ~200ns. Therefore you may find that for your particular workload, your program is faster with a lower GOMAXPROCS value. Always profile and benchmark your programs and make sure the Go runtime configuration is only changed if absolutely required.

Summary

I hope you found this useful and you came away with a better understanding of what goroutines are how they are scheduled. For more Go tidbits, or to say hey, you find me on Twitter here

Further Reading

As well as the links referenced above, I found the following blogs/articles incredibly valuable whilst researching this blogpost, and I highly recommend reading them if you want to dive a little bit further into this topic.

💖 💪 🙅 🚩
mattjamesboyle
Matt Boyle

Posted on April 4, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related