Concurrency vs Parallelism Explained (With Go Examples)

Concurrency is about structure, parallelism about execution. The difference explained with runnable Go examples, when each one speeds up a program, and when neither does.

Concurrency and parallelism get used as synonyms, and most of the confusion about goroutines, threads and async code comes from that. They are different ideas, they solve different problems, and a program can have either one without the other. The video above walks through it on a whiteboard; this is the written version with Go examples you can run.

The one-sentence difference

Concurrency is about structure: dealing with many things at once. Parallelism is about execution: doing many things at once.

Concurrency is a property of your program’s design. You split work into independent tasks that can make progress without waiting on each other, and you compose them. Whether those tasks actually run simultaneously is a separate question.

Parallelism is a property of the hardware and the runtime. If you have four cores and four runnable tasks, they can execute at the same instant. If you have one core, the most you can do is interleave them very quickly, which still looks like “at once” to a human but is not parallel.

Rob Pike’s formulation is the one worth remembering: concurrency is not parallelism, but concurrency makes parallelism easy to obtain.

A kitchen, not a highway

A single cook preparing three dishes is concurrent. They chop onions while water boils, stir a sauce while bread is in the oven. Only one pair of hands is working, but nothing sits idle waiting for the slowest step.

Three cooks each preparing one dish is parallel. Three pairs of hands, three things genuinely happening at the same moment.

Three cooks who all need the same knife are parallel hardware with a concurrency problem: the design forces them to wait on a shared resource, so the extra hands buy you very little.

What concurrency buys you

Most programs spend most of their time waiting: for the network, for the disk, for a database, for a user. A concurrent design lets the program do something useful during that wait instead of blocking.

func fetchAll(urls []string) []string {
    results := make([]string, len(urls))
    var wg sync.WaitGroup

    for i, u := range urls {
        wg.Add(1)
        go func(i int, u string) {
            defer wg.Done()
            resp, err := http.Get(u)
            if err != nil {
                results[i] = err.Error()
                return
            }
            defer resp.Body.Close()
            results[i] = resp.Status
        }(i, u)
    }

    wg.Wait()
    return results
}

Ten URLs fetched this way take roughly as long as the slowest single request rather than the sum of all ten. That speed-up has nothing to do with how many cores you have. Run it with GOMAXPROCS=1 and it is just as fast, because the goroutines spend almost all their time waiting on sockets, and the scheduler hands the single core to whichever one has data ready. This is concurrency paying off on I/O-bound work.

What parallelism buys you

Parallelism helps when the bottleneck is the CPU itself. Summing a very large slice, encoding video, hashing passwords: there is no waiting to hide, only arithmetic to do, so the only way to finish sooner is to do arithmetic on more cores at the same time.

func sumParallel(nums []int, workers int) int {
    chunk := (len(nums) + workers - 1) / workers
    partial := make(chan int, workers)

    for w := 0; w < workers; w++ {
        lo := w * chunk
        hi := min(lo+chunk, len(nums))
        go func(part []int) {
            s := 0
            for _, n := range part {
                s += n
            }
            partial <- s
        }(nums[lo:hi])
    }

    total := 0
    for w := 0; w < workers; w++ {
        total += <-partial
    }
    return total
}

This code is also concurrent (it is structured as independent tasks), but it only gets faster when those tasks run in parallel. With GOMAXPROCS=1 it is no quicker than a plain loop, and slightly slower because of the goroutine and channel overhead. With eight cores and workers = 8 it approaches an 8x speed-up on a large enough slice.

Why Go makes this confusing, and then easy

Go blurs the line on purpose. You write concurrent code with goroutines and channels, and the runtime decides how much parallelism to apply, up to GOMAXPROCS (which defaults to the number of CPUs). The same program is concurrent-but-sequential on one core and concurrent-and-parallel on sixteen, with no code changes.

That is the sense in which concurrency “makes parallelism easy”: if the program is already decomposed into independent pieces, giving it more cores is a runtime knob rather than a rewrite. The reverse is not true. A program written as one long sequential loop cannot be parallelised by adding cores; it has to be restructured first.

The trade-offs nobody mentions in the definitions

  • Concurrency adds coordination. As soon as two tasks touch the same memory you need a mutex, a channel or an atomic, and you have introduced the possibility of races and deadlocks. Run your tests with go test -race.
  • Parallelism has a ceiling. Amdahl’s law: if 10 percent of your work is inherently sequential, no number of cores gets you past a 10x speed-up. Measure before you add workers.
  • More goroutines is not more speed. For CPU-bound work, more goroutines than cores just adds scheduling overhead. For I/O-bound work, the limit is usually the remote system, not your process; a semaphore (a buffered channel) to cap in-flight requests is often the first thing you add.
  • Both can make a program slower. A tiny workload split across workers can take longer than the straight loop, because spawning and synchronising cost more than the work saved.

A quick decision guide

Your bottleneck is Reach for What it changes
Waiting on I/O (network, disk, DB) Concurrency Latency: overlap the waits
Raw CPU work Parallelism Throughput: use more cores
Both, in a server Concurrency per request, parallelism across requests Go’s default model already does this
Nothing measurable Neither yet Profile first (go tool pprof)

Takeaways

Concurrency is how you structure a program so that independent things do not wait on each other. Parallelism is whether those things execute simultaneously, which depends on cores and the runtime. In Go you write the former and get the latter for free when hardware allows. The design question is always the same: what is the program actually waiting on? Answer that, and the choice makes itself.