author
Kevin Kelche

Concurrency and Goroutines in Golang


Introduction

What is concurrency?

Concurrency can be described as the composition of independently executing processes or tasks. In other words, concurrency is the ability to run multiple tasks at the same time. For instance, if you are watching a movie and you are also texting a friend, you are doing two things at the same time but not simultaneously. This is concurrency.

Concurrency is different from parallelism. Parallelism is the ability to run multiple tasks simultaneously. For instance, a person can dance and sing at the same time. This is parallelism because the person is doing two things simultaneously.

Even though Golang is a concurrent programming language, it doesn’t mean that you can’t write parallel programs in Golang.

Concurrency in Golang

Golang is termed as a concurrent programming language since it has a built-in concurrency model achieved through goroutines and channels.

Goroutines

Goroutines are lightweight threads, they are multiplexed onto multiple kernel threads and are managed by the Go runtime which is responsible for scheduling goroutines onto kernel threads.

They are termed lightweight since the overhead of creating a goroutine is very small and cheap, unlike spinning up another thread in the kernel space.

You can learn more about the Golang runtime scheduler here.

How to create a goroutine

Golang provides an easy API to create a goroutine. The keyword go is used to create a goroutine and is placed in front of the function call.

Note: The main function is also a goroutine referred to as the main goroutine.

main.go
package main

import "fmt"

func main() {
  go func() {
    fmt.Println("Hello from goroutine")
  }()
  fmt.Println("Hello from main")
}

Copied!

By running this program you will see that it only prints Hello from main and not the message from the goroutine. This is because the main goroutine exits before the other goroutine can print the message since it is not blocking. To fix this, we can use a timer to block the main goroutine for a certain amount of time.

main.go
package main

import (
  "fmt"
  "time"
)

func main() {
  go func() {
    fmt.Println("Hello from goroutine")
  }()
  time.Sleep(1 * time.Second)
  fmt.Println("Hello from main")
}

Copied!

The timer is not the best way to block the main goroutine from exiting. We can use the WaitGroup function. This introduces the topic of synchronization.

Synchronization

WaitGroup

WaitGroup is a synchronization primitive that allows us to wait for a collection of goroutines to finish. It is provided by the sync package.

Let’s solve the previous problem using WaitGroup.

main.go
package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  wg.Add(1)
  go func() {
    fmt.Println("Hello from goroutine")
    wg.Done()
  }()
  wg.Wait()
  fmt.Println("Hello from main")
}

Copied!

The WaitGroup is just a counter that keeps track of the number of goroutines that are waiting to finish. The Add function increments the counter by the number of goroutines that are waiting to finish, and the Done function decrements the counter by 1, thus the need to call this function inside the goroutine. The Wait function blocks the main goroutine until the counter is 0.

To demonstrate a better use case of WaitGroup and synchronization problems in general, let’s write a program that calculates the Fibonacci sequence.

main.go
package main

import (
  "fmt"
  "sync"
)

func fibonacci(n int){
  fib := make([]int, n)
  fib[0] = 0
  fib[1] = 1
  for i := 2; i < n; i++ {
    fib[i] = fib[i-1] + fib[i-2]
  }

  fmt.Println(fib)
}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go func() {
    fibonacci(10)
    wg.Done()
  }()
  go func() {
    fibonacci(20)
    wg.Done()
  }()
  wg.Wait()
}

Copied!

In this example, we are creating two goroutines that calculate the Fibonacci sequence. The program will wait for both goroutines to finish before exiting.

Note:

The order of execution of a goroutine is non-deterministic.

Channels

Channels are a typed conduit through which you can send and receive values with the channel operator, <-. They can be used to synchronize execution across goroutines.

main.go
package main

import "fmt"

func fib(n int, c chan int) {
  x, y := 0, 1
  for i := 0; i < n; i++ {
    c <- x
    x, y = y, x+y
  }
  close(c)
}

func main() {
  c := make(chan int, 10)
  d := make(chan int, 20)
  go fib(cap(c), c)
  go fib(cap(d), d)
  for i := range c {
    fmt.Printf("%d ,", i)
  }
  fmt.Println()
  for i := range d {
    fmt.Printf("%d ,", i)
  }
}

Copied!

In this example, we are creating two goroutines that calculate the Fibonacci sequence and send the result to two channels. The range keyword is used to iterate over the channel and print the result. The Close function is used to close the channel when the goroutine is done sending the result, this is important because the range keyword will keep on iterating over the channel until it is closed.

To explore more about channels, you can read more in my earlier article here.

Mutex

Mutex is another synchronization mechanism that is used to provide exclusive access to a resource. It is also provided by the sync package.

main.go
package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  var m sync.Mutex
  counter := 0
  for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
      m.Lock()
      counter++
      m.Unlock()
      wg.Done()
    }()
  }
  wg.Wait()
  fmt.Println(counter)
}

Copied!

In this program, we are creating a goroutine that increments the counter by 1. The Lock function is used to lock the mutex and will block the goroutine untill the mutex is unlocked by the Unlock function. I cover more about mutex in my article here.

Conclusion

In this article, we have learned about goroutines and how to handle synchronization problems using WaitGroup, Channels, and Mutex. This is not all about goroutines, there are some advanced topics that we have not covered in this article linked below.

Subscribe to my newsletter

Get the latest posts delivered right to your inbox.