Skip to content

Goroutines

Johnny Boursiquot edited this page May 31, 2024 · 3 revisions

Key Concepts

  1. Lightweight threads managed by the Go runtime.
  2. Multiplexed by the runtime over a smaller number of OS threads, making them much cheaper (memory, performance) compared to traditional threads.
  3. Dynamic stack growth (they start small at only a few kilobyes).
  4. Enable parallelism by being run across CPU cores (scheduler manages this).
  5. Efficient communication through channels for safe data exchange without explicit locks.
  6. Require synchronization.

Goroutines illustrated

Concurrency is about dealing with lots of things at once (e.g., multiple tasks making progress). Parallelism is about doing lots of things at once (e.g., multiple tasks running simultaneously on different CPU cores).

Example

The first iteration of this example invokes sum within the main goroutine.

package main

import (
	"fmt"
)

// sum calculates and prints the sum of numbers
func sum(nums []int) {
	sum := 0
	for _, v := range nums {
		sum += v
	}
	fmt.Println("Result:", sum)
}

func main() {
	nums := []int{1, 2, 3, 4, 5}
	sum(nums)

	// invoke the sum function as a goroutine
	//
}

This next iteration invokes sum in its own goroutine.

package main

import (
	"fmt"
	"time"
)

// sum calculates and prints the sum of numbers
func sum(nums []int) {
	sum := 0
	for _, v := range nums {
		sum += v
	}
	fmt.Println("Result:", sum)
}

func main() {
	nums := []int{1, 2, 3, 4, 5}
	// sum(nums)

	// invoke the sum function as a goroutine
	go sum(nums)

	// force main thread to sleep
	time.Sleep(100 * time.Millisecond)
}

Discussion:

  • Why do we need time.Sleep?
  • What would happen without the sleep?

Synchronization

Go's standard library includes package sync which provides basic synchronization primitives, including mutual exclusion locks, as well as handy features such as the WaitGroup which helps us wait for a collection of goroutines to finish their work.

package main

import (
	"fmt"
	"sync"
)

func main() {
	// given a list of names
	names := []string{"James", "Toni", "Maya"}

	// initialize a map to store the length of each name
	namesMap := make(map[string]int, len(names))

	// initialize a wait group for the goroutines that will be launched
	var wg sync.WaitGroup
	wg.Add(len(names))

	var mu sync.Mutex

	// launch a goroutine for each name we want to process
	for _, name := range names {
		go func(name string) {
			defer wg.Done()
			mu.Lock()
			defer mu.Unlock()
			namesMap[name] = len(name)
		}(name)
	}

	// wait for all goroutines to finish
	wg.Wait()

	// print the map
	fmt.Println(namesMap)
}

About mutexes

  • Mutexes are ideal for protecting access to shared data structures accessed by multiple goroutines without the need for communication.
  • Mutexes are simpler when dealing with straightforward critical sections.

Discussion

  • What would happen without the locks?
  • How do I catch possible data races in my programs during development?

Exercise

See exercise 1.

Resources

Clone this wiki locally