Skip to main content

Go from scratch, part 11: goroutines and the CSP model

Curso: Go-en — Lección 11 de 15

> series: learn go from 0 to 100 | article 11 of 19

Everything so far was the warm-up. Go concurrency is why the language exists and probably why you’re reading this series. And it starts with two letters: go.


A goroutine is a function flying solo

Put go in front of any call and that function runs concurrently, without blocking its launcher (A Tour of Go, goroutines):

func tarea(nombre string) {
  pausa := 10 * time.Millisecond
  for i := 1; i < 4; i++ {
    fmt.Println(nombre, i)
    time.Sleep(pausa)
  }
}
func main() {
  go task("goroutine") // the go keyword and off it flies
  task("main")
  // when main returns, ALL goroutines die
}

Two warnings from the example:

First: when main returns, the program dies with every goroutine inside, finished or not.

Second: that time.Sleep as a waiting mechanism is a pedagogical hack. The right way (channels, WaitGroup) arrives in articles 12 and 14.

Why they’re not threads

A goroutine, the basic unit of Go concurrency, is not an OS thread. It’s far cheaper. It starts with a stack of a few KB that grows and shrinks on demand, and the Go runtime multiplexes thousands of them onto a handful of real threads (Effective Go, goroutines and FAQ, why goroutines).

The official FAQ talks about it being practical to create hundreds of thousands in the same address space. Try that with Java threads and tell me how the RAM bill goes.

Comparison with your world: Python’s GIL leaves CPU concurrency limping, and JS is single-threaded with an event loop. In Go you write plain sequential code, no async, no await, no function colors, and the scheduler does the magic.

Go concurrency: Pixelart diagram illustrating Go concurrency: eight small goroutines distributed by the Go scheduler over three OS threads
[Thousands of goroutines, a handful of threads: the Go scheduler does the dealing.]

CSP: the philosophy behind it

Go’s concurrency model descends from C.A.R. Hoare’s 1978 paper Communicating Sequential Processes, as the official FAQ acknowledges. The idea: instead of sharing memory and guarding it with locks, processes communicate by passing messages.

The official slogan sums it up better than I could: “Do not communicate by sharing memory; instead, share memory by communicating” (Effective Go and the Go blog). In plain words: don’t hand two goroutines the same variable plus a mutex, hand the data to one and have it pass it through a channel when it’s time.

The enemy: the data race

This is what happens when the advice above gets ignored:

counter := 0
for i := 0; i < 1000; i++ {
  go func() {
    counter++ // 1000 goroutines stepping on each other
  }()
}
time.Sleep(time.Second)
fmt.Println(counter) // 1000? almost never

counter++ is three operations (read, add, write) and a thousand goroutines interleaving them lose increments along the way. The worst part: sometimes it comes out right and the bug hides for months.

Go ships a built-in race detector: run with go run -race . or go test -race and the runtime will call out races it detects at runtime (race detector docs). Starting today: your concurrent code tests always carry -race.


Weekly challenge: put Go concurrency to work

  • Launch five numbered goroutines printing their number three times and watch the interleaving. Run it several times: the order shifts.
  • Reproduce the counter data race and confirm it with go run -race .. Keep the program: we’ll fix it twice, with channels (article 12) and with a mutex (article 14).
  • Bonus: measure how many sleeping goroutines your machine can create before sweating. Go up in steps of 10,000 and watch the process memory.

Go concurrency: first level complete.

Next article: channels and select, the pipes goroutines talk through.

> exit
Retrato pixel art de Jenniffer Cubillos

thanks for reading