Goroutines

Goroutines

A beginner's guide

Goroutines are lightweight processes managed by the Go runtime. They are a way to achieve concurrency in programs.

What is a process?

A process is an instance of a program that is currently being executed by a computer's operating system. The operating system associates some resources, such as memory, with the process and makes sure that other processes can’t access them. A process is composed of one or more threads.

What is a thread?

A thread is a unit of execution that is given some time to run by the operating system. Threads within a process share access to resources. A CPU can execute instructions from one or more threads at the same time, depending on the number of cores. One of the jobs of an operating system is to schedule threads on the CPU to make sure that every process (and every thread within a process) gets a chance to run.

Difference between a Normal Program and a Program utilizing Goroutine:

A goroutine is launched by placing the go keyword before a function invocation.
Suppose we have a program that has two independent functions. In a normal program, functions are executed synchronously i.e. the second function will execute only when the first one has finished executing.

However, since both functions are independent, it would be efficient if we could execute both functions together asynchronously.
For this, we use goroutines.

How does Goroutines achieve concurrency?

We know Goroutines are lightweight processes managed by the Go runtime. When we run a Go program, the Go runtime creates a number of threads and launches a single goroutine to run our program.

All of the goroutines created by your program, including the initial one, are assigned to these threads automatically by the Go run-time scheduler.

What benefits does it have over the underlying operating system scheduler?

  • Goroutine creation is faster than thread creation because we aren’t creating an operating system–level resource.

  • Goroutine initial stack sizes are smaller than thread stack sizes and can grow as needed. This makes goroutines more memory efficient.

  • Switching between goroutines is faster than switching between threads because it happens entirely within the process, avoiding operating system calls that are (relatively) slow.

  • The scheduler is able to optimize its decisions because it is part of the Go process.
    The scheduler works with the network poller, detecting when a goroutine can be unscheduled because it is blocking on I/O. It also integrates with the garbage collector, making sure that work is properly balanced across all of the operating system threads assigned to our Go process.

Create Goroutine:

package main

import "fmt"

func main() {
    go greetings("User 1 ")
    greetings("User 2 ")
}

func greetings(name string) {
    fmt.Printf("Hello %s ", name)
}

In the above example, greetings("User 1 ") is a goroutine. But when we run it:

 ~/go run main.go
Hello User 2  %

Only User 2 output is printed.
This is because we have used go with the first function call, so it is treated as a goroutine. The goroutine runs independently and the main() function now runs concurrently. Hence, the second call is executed immediately and the program terminates without completing the first function call.

Solution:

If we are waiting for a single goroutine we can use a done channel pattern. But if we are waiting on several goroutines, we need to use a WaitGroup, which is found in the sync package in the standard library.

Using WaitGroups:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(3)
    go func() {
        defer wg.Done()
        doThing1()
    }()
    go func() {
        defer wg.Done()
        doThing2()
    }()
    go func() {
        defer wg.Done()
        doThing3()
    }()
    wg.Wait()
}

func doThing1() {
    fmt.Println("Thing 1 done!")
}

func doThing2() {
    fmt.Println("Thing 2 done!")
}

func doThing3() {
    fmt.Println("Thing 3 done!")
}

A sync.WaitGroup doesn’t need to be initialized, just declared, as it's zero value is useful. There are three methods on sync.WaitGroup:

  • Add, which increments the counter of goroutines to wait for.

  • Done, which decrements the counter and is called by a goroutine when it is finished.

  • Wait, which pauses its goroutine until the counter hits zero.

    Add is usually called once, with the number of goroutines that will be launched. Done is called within the goroutine. To ensure that it is called, even if the goroutine panics, we use a defer.