liveBook · Manning

Concurrency

As explained in 100 Go Mistakes and How to Avoid Them

Concurrent execution is the overlapping progress of two or more threads or computations during the same time period.

A common mistake is treating concurrency as a promise of faster execution. Concurrency structures independent activities so they can make progress during overlapping periods; parallelism executes work at the same time. A concurrent design need not be parallel—or faster. section 8.1 section 8.2 section 8.2.1

Reorganizing a coffee shop

Giving a second waiter a second coffee machine duplicates service capacity: that is parallelism. Instead, one waiter can accept orders while another grinds beans for a single machine. The roles now overlap and must coordinate, but the machine has not been duplicated. That is a concurrent structure. section 8.1

Figure 8.3 Splitting the role of the waiters

The split waiter roles and additional queue make the structural change visible without implying another coffee machine. section 8.1

In Go, goroutines provide application-level units of execution scheduled by the runtime onto OS threads. Whether adding them helps depends on available parallel work, workload size, and contention—not merely on the number started. section 8.2.1 section 8.2

When parallel merge sort loses

Two independent halves of a slice can be sorted in separate goroutines, with sync.WaitGroup holding the merge until both finish. Recursively starting goroutines for every split looks promising, yet a benchmark on a four-core machine with 10,000 elements showed this version nearly an order of magnitude slower than sequential merge sort. section 8.2.2

func parallelMergesortV1(s []int) {
    if len(s) <= 1 {
        return
    }
 
    middle := len(s) / 2
 
    var wg sync.WaitGroup
    wg.Add(2)
 
    go func() {             #1
        defer wg.Done()
        parallelMergesortV1(s[:middle])
    }()
 
    go func() {             #2
        defer wg.Done()
        parallelMergesortV1(s[middle:])
    }()
 
    wg.Wait()
    merge(s, middle)        #3
}

The first implementation starts two goroutines at each recursive split and waits before merging. section 8.2.2

Use a cutoff for small slices: sort them sequentially and reserve goroutines for larger splits. In the supplied benchmark, a version with a 2,048-element cutoff beat both the unrestricted parallel version and the sequential version. Benchmark the alternatives rather than assuming the cutoff or concurrency will help every workload. section 8.2.2 section 8.7

const max = 2048                      #1
 
func parallelMergesortV2(s []int) {
    if len(s) <= 1 {
        return
    }
 
    if len(s) <= max {
        sequentialMergesort(s)        #2
    } else {                          #3
        middle := len(s) / 2
 
        var wg sync.WaitGroup
        wg.Add(2)
 
        go func() {
            defer wg.Done()
            parallelMergesortV2(s[:middle])
        }()
 
        go func() {
            defer wg.Done()
            parallelMergesortV2(s[middle:])
        }()
 
        wg.Wait()
        merge(s, middle)
    }
}

The revised implementation switches to sequential sorting at the cutoff. section 8.2.2

Choose the coordination mechanism for the interaction:

  • Use channels for coordination and orchestration between concurrent goroutines; use mutexes when parallel goroutines need synchronization. Neither choice removes the need to reason about blocking and ordering. section 8.7 section 1.2
  • Distinguish data races from race conditions: being free of simultaneous conflicting memory access does not guarantee deterministic behavior. section 8.7
  • For CPU-bound work, keep goroutine counts near GOMAXPROCS; for I/O-bound work, consider constraints such as the external system. Use contexts when work needs a deadline or cancellation signal. section 8.7

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Defines concurrency against parallelism, traces Go scheduling, and works through benchmarked merge-sort implementations without fully elaborating sync.WaitGroup.
Examples
Contrasts coffee-shop staffing designs and benchmarks sequential, unrestricted parallel, and cutoff-based merge sort.
What it assumes
Introduces concurrency and parallelism through an analogy before Go-specific details, while only briefly defining sync.WaitGroup.
What you can do after
Evaluate/understand it by benchmarking sequential and concurrent designs, then selecting coordination or synchronization according to goroutine interactions.
How it connects
Connects goroutines, parallelism, scheduling, channels, mutexes, data races, race conditions, and contexts.

Also explained in

Compare how 23 books explain concurrency →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.