Grokking Concurrency in Go
Vote

0% completed

​

Concurrency and Parallelism

Concurrency

Parallelism

Cores Available to a Go Program

A Concurrent Program on One Core

Parallel Execution

Comparing the Two

A Common Mistake

Using This in an Interview

Key Takeaways

A cook in a small kitchen is making soup and rice. The cook does not stand and watch the soup while it simmers. Instead, the cook stirs the soup, starts the rice, returns to the soup, and checks the rice again. Both dishes are in progress over the same half hour, even though only one pair of hands is working.

A kitchen with two cooks works differently. One cook stays with the soup and the other stays with the rice, so at any given second both dishes are receiving work. The first kitchen handles two dishes by switching between them, while the second kitchen does two pieces of work at the same instant.

Go uses two separate words for these two kitchens. This page defines both words, shows a Go program that behaves like each kitchen, and explains why the first kind of program needs only one CPU core.

Concurrency

Organizing a program as several tasks that each make progress over the same period of time is called concurrency (dealing with many tasks by switching between them). A concurrent program does not require the tasks to run at the same instant. It requires only that a task which is waiting does not stop the other tasks from moving forward.

In Go, each task is usually a goroutine, which is a function started with the go keyword that runs independently of the code that started it. The Go runtime switches a CPU core from one goroutine to another, most often when the current goroutine has to wait. A goroutine waits when it sleeps, when it reads from the network or a disk, or when it sends to or receives from a channel that is not ready.

Concurrency is a property of how the program is written. The same concurrent program can run on one core or on sixteen cores without any change to its code.

Parallelism

Executing several tasks at the same instant is called parallelism (doing many tasks at once on several CPU cores). Parallelism needs hardware with more than one core, because a single core executes the instructions of only one task at a time. Parallelism is therefore a property of how the program is run, not of how it is written.

A concurrent Go program becomes parallel when the runtime has more than one core available and more than one goroutine has work to do. The goroutines are the same in both cases. What changes is whether their instructions overlap in time.

Concurrency versus parallelism
Concurrency versus parallelism

Cores Available to a Go Program

Two values in the runtime package describe the cores a program may use. The function runtime.NumCPU returns the number of logical CPUs the process can use. A logical CPU is one stream of instructions that the hardware can run; a core that runs two such streams counts as two logical CPUs. The function runtime.GOMAXPROCS sets the maximum number of CPUs that can execute Go code at the same time, and it returns the previous setting.

Go
Go

. . . .

One possible output (measured on one computer with 8 cores and 16 logical CPUs):

logical CPUs: 16 GOMAXPROCS: 16

By default, GOMAXPROCS follows the number of logical CPUs, so a program on this computer can run Go code on 16 logical CPUs at the same time. Since Go 1.25, the default on Linux also takes into account any CPU limit placed on a container. You rarely need to change the value. Setting it to 1, however, is a useful way to see concurrency with no parallelism at all.

A Concurrent Program on One Core

The program below models the first kitchen. Each dish is a goroutine, and each step of a dish is a sleep, which stands in for waiting while the soup simmers. The soup has three steps of 100 ms, and the rice has two steps of 140 ms. The call runtime.GOMAXPROCS(1) allows only one core to run Go code, so the two goroutines can never execute instructions at the same instant.

Go
Go

. . . .

Output:

soup step 1 done at 100ms rice step 1 done at 140ms soup step 2 done at 200ms rice step 2 done at 280ms soup step 3 done at 300ms both dishes ready at 300ms

The steps of the two dishes alternate, and both dishes are ready at 300 ms. Cooked one after the other, they would need 300 ms for the soup plus 280 ms for the rice, which is 580 ms. The main function receives from done twice, so it waits until both goroutines have reported that they finished.

The single core was idle for almost all of those 300 ms. While the soup goroutine sleeps, it needs no CPU, so the runtime runs the rice goroutine. When the rice goroutine sleeps as well, the runtime has nothing to run until one of the sleeps ends. This is why concurrency helps a program that spends most of its time waiting, even on one core.

One core, two goroutines over time
One core, two goroutines over time

Parallel Execution

Remove the call to runtime.GOMAXPROCS(1) and the program prints the same lines with the same times. The two goroutines may now run on different cores. However, each one uses very little CPU time per step, so the extra cores have almost nothing to do. Parallelism adds speed only when several goroutines need the CPU for long stretches at the same time.

A goroutine that adds up a billion numbers is an example of such work. It never waits, so switching between two of them on one core gives each one half of the core and finishes no sooner. Two such goroutines on two cores, by contrast, can finish in about half the time.

Comparing the Two

ConcurrencyParallelism
MeaningMany tasks make progress over the same periodMany tasks execute at the same instant
Property ofHow the program is writtenHow the program is run
Needs more than one coreNoYes
Helps most when tasksSpend time waitingSpend time computing
In GoGoroutines and channelsGoroutines on several cores, up to GOMAXPROCS

A Common Mistake

A frequent mistake is to assume that starting goroutines makes a program faster. Goroutines make a program concurrent, and they let it overlap waiting time. They make it faster at computation only when free cores are available to run them. Even then, each goroutine needs enough work to justify the cost of starting it.

The opposite mistake also appears. Some engineers assume that a concurrent program is safe from timing problems if GOMAXPROCS is 1. It is not. The runtime can still switch between goroutines at many points, so two goroutines that share data can still interfere with each other on a single core.

Using This in an Interview

Concurrency is dealing with many tasks over the same period by switching between them, and parallelism is executing many tasks at the same instant on several cores. A Go program written with goroutines is concurrent, and it runs in parallel when more than one core is available. A likely follow-up asks whether a concurrent program can be useful on one core. It can, when its tasks spend most of their time waiting, because the core runs one task while the others wait.

Key Takeaways

  • Concurrency means several tasks make progress over the same period, with the runtime switching between them.
  • Parallelism means several tasks execute at the same instant, which needs more than one CPU core.
  • runtime.NumCPU reports the logical CPUs available, and runtime.GOMAXPROCS(0) reports how many may run Go code at once.
  • A concurrent program on one core still finishes sooner when its tasks spend their time waiting.
  • More cores help only work that keeps the CPU busy.

Concurrency describes how a program is organized: as separate tasks that can wait without stopping each other. Parallelism describes how the hardware runs those tasks. A Go program runs in parallel whenever more than one core is available and more than one goroutine has work ready.

Reading Progress

0%


Vote for new content

On This Page

Concurrency

Parallelism

Cores Available to a Go Program

A Concurrent Program on One Core

Parallel Execution

Comparing the Two

A Common Mistake

Using This in an Interview

Key Takeaways