You are viewing a free preview of this lesson.
Subscribe to unlock all 10 lessons in this course and every other course on LearningBro.
Thinking concurrently is the computational-thinking skill of spotting where the parts of a problem can make progress at the same time rather than one after another — and, just as importantly, recognising where they cannot, because they depend on each other or share something they must not touch simultaneously. A web server that handled one visitor at a time would be useless; a games console that rendered graphics, read the controller, ran the game logic and played audio strictly in sequence would stutter. The power of concurrency is overlap: while one task waits for a slow disk, another can use the processor; while one core sorts the left half of a list, another sorts the right. But overlap brings hazards — two tasks updating the same balance, or each waiting forever for a resource the other holds — that simply do not exist in sequential code.
This lesson develops the H446 view of concurrent thinking: the distinction between sequential, concurrent and parallel execution; where concurrency genuinely helps and where it does not; and the benefits and drawbacks, including the classic hazards of race conditions and deadlock and the synchronisation mechanisms that tame them. The hardware of parallelism — multiple cores, GPUs, SIMD and the quantitative limit of Amdahl's Law — is developed in the Processor Types lesson of the Processors & Hardware course, and the way a single processor time-slices between tasks is covered by scheduling algorithms in Software Systems; this lesson focuses on the thinking — deciding what can overlap and managing the consequences — and cross-links to that material rather than re-deriving it.
This lesson addresses H446 section 2.1.5 — Thinking Concurrently. The specification expects you to:
It links tightly to parallel processing and Amdahl's Law (1.1.1/1.1.2, Processor Types lesson), process scheduling (1.2.3), transaction processing/ACID in databases (1.3.2), and the dependency analysis of Thinking Procedurally (2.1.3) — independent decomposition branches are precisely the candidates for concurrency.
Three terms are easy to blur and worth nailing down, because examiners test the distinction directly.
| Term | Meaning | Hardware needed |
|---|---|---|
| Sequential | Tasks run strictly one after another; one finishes before the next starts | one core suffices |
| Concurrent | Several tasks are in progress during the same period; they may be interleaved on one core, not truly simultaneous | one core (by switching) or more |
| Parallel | Several tasks execute at exactly the same instant | requires multiple cores/processors |
The crucial subtlety is that concurrency is about structure — dealing with many things at once — while parallelism is about execution — doing many things at the same instant. A single-core processor achieves concurrency by time-slicing: it runs task A for a few milliseconds, switches to task B, back to A, and so on, fast enough that all appear to progress together even though only one instruction executes at any instant. True parallelism needs genuinely separate hardware. Every parallel system is concurrent, but not every concurrent system is parallel.
gantt
title Sequential vs Concurrent (1 core) vs Parallel (2 cores)
dateFormat X
axisFormat %s
section Sequential
Task A :0, 3
Task B :3, 6
section Concurrent (interleaved)
A :0, 1
B :1, 2
A :2, 3
B :3, 4
section Parallel core 1
Task A :0, 3
section Parallel core 2
Task B :0, 3
Read the diagram top to bottom: sequential runs A then B back-to-back; concurrent interleaves slices of A and B on one timeline (so both are "in progress" but the total time is no shorter); parallel runs A and B on two separate cores at once, genuinely halving the wall-clock time. That difference — concurrency reorganises when work happens, parallelism reduces how long it takes — is the heart of the topic.
The single most important judgement in concurrent thinking is whether two tasks are independent. Two tasks are independent if neither needs the other's output and they do not interfere through shared data; only independent tasks can safely overlap.
Key Term: Tasks are independent if neither requires the output of the other and they do not conflict over shared resources. Independent tasks are the candidates for concurrency; dependent tasks must be ordered.
| Scenario | Concurrent? | Why |
|---|---|---|
| Download three separate files | Yes | Each download is independent |
| Render different regions of one image | Yes | Regions do not affect one another (data parallelism) |
| Search three separate database shards | Yes | Each search is independent |
Compute A + B, then (A + B) * C | No | The multiply depends on the add's result |
| Write to a file while another task reads it | No (unsafe) | They conflict over the same shared resource |
| Sum a list where each step adds to a running total | No | Each step depends on the previous total |
This is exactly the dependency reasoning of Thinking Procedurally: draw the dependency graph, and any tasks with no path between them can run concurrently, while any chain of dependencies must run in order. Concurrency does not help — and may actively hurt — when:
Recognising these limits is as much a part of thinking concurrently as spotting opportunities — naively "making everything concurrent" is a classic novice error that adds complexity and bugs for no gain.
Worked example — summing a large array. Suppose you must add up a million numbers. Done sequentially, you accumulate a single running total, and each addition depends on the previous total — apparently a hopeless candidate for concurrency. But thinking concurrently reframes the problem: split the array into, say, four chunks, have four workers independently sum their own chunk (these four partial sums share nothing, so they are independent and can run in parallel), then add the four partial results together at the end. The bulk of the work — the four chunk-sums — is now concurrent; only the tiny final combine is serial. This map-then-reduce shape (independent work per chunk, then a small combine) is one of the most important patterns in concurrent thinking, and it is exactly how parallel hardware and frameworks like MapReduce extract speed from data that looked stubbornly sequential. The lesson is that independence is often not given but engineered by restructuring the problem so that large parts become independent.
The flip side is granularity — how big each concurrent task is. Tasks that are too fine-grained (each does only a trivial amount of work) spend more time being created and synchronised than computing, so the overhead swamps the benefit. Tasks that are too coarse-grained (a few huge tasks) may not keep all the cores busy and can leave some idle while one long task finishes. Choosing a sensible task size — enough work per task to dwarf the scheduling overhead, but enough tasks to occupy every core — is a key judgement when deciding how to make a problem concurrent, not just whether to.
| Benefit | Explanation | Example |
|---|---|---|
| Higher throughput | More work completes per unit time when tasks overlap | A web server serves thousands of requests "at once" |
| Better resource use | While one task waits on slow I/O, another uses the CPU | A program processes input while a file loads |
| Responsiveness | Background work does not freeze the user interface | A document autosaves while you keep typing |
| Scalability | Adding cores increases throughput for parallelisable work | A multi-core server handles more traffic |
The resource-use benefit is especially important and underrated. Much real work is I/O-bound — waiting on disks, networks or users — and during those waits the processor would otherwise sit idle. Concurrency lets that idle time be filled with other useful work, so even a single core gains hugely from concurrency on I/O-bound workloads, entirely separately from any parallel-hardware speed-up.
It is worth being precise about which kind of workload benefits from which kind of concurrency, because exam answers often conflate them. I/O-bound work (a web server waiting on network and database) benefits from concurrency even on one core, because the gains come from overlapping waiting with useful work — while one request waits for the database, the core serves another. CPU-bound work (heavy computation that keeps the processor genuinely busy) gains little from single-core concurrency, because there is no idle waiting time to fill; it benefits only from true parallelism on multiple cores, where the computation is genuinely divided. A strong concurrent thinker therefore asks first "is this task waiting a lot, or computing a lot?" — the answer determines whether plain concurrency on one core will help, or whether multiple cores are required to see any speed-up at all.
The moment two concurrent tasks share mutable data, new failure modes appear that cannot occur in sequential code. The first is the race condition: the outcome depends on the timing or interleaving of operations, which is not guaranteed, so the program produces different results on different runs.
The canonical example is two withdrawals from one account (starting balance £100), where each does read balance → compute → write balance:
| Time | Process A | Process B | Stored balance |
|---|---|---|---|
| 1 | read balance → 100 | 100 | |
| 2 | read balance → 100 | 100 | |
| 3 | compute 100 − 80 = 20 | 100 | |
| 4 | compute 100 − 60 = 40 | 100 | |
| 5 | write 20 | 20 | |
| 6 | write 40 | 40 |
The account ends at £40, yet £80 + £60 = £140 was withdrawn from £100 — money has been created from nothing. The fault is that B read the balance before A had written its update, so B's calculation used a stale value and then overwrote A's. The block of code that reads-modifies-writes the shared balance is a critical section: it must be executed by only one task at a time. Run sequentially the bug is impossible; only the interleaving of concurrent access exposes it, which is why race conditions are so insidious — they may appear only occasionally, under timing that is hard to reproduce.
The second classic hazard is deadlock: two or more tasks are each blocked waiting for a resource the other holds, so none can ever proceed. Deadlock requires all four of the following conditions simultaneously (the Coffman conditions):
| Condition | Meaning |
|---|---|
| Mutual exclusion | A resource can be held by only one task at a time |
| Hold and wait | A task holds one resource while waiting for another |
| No pre-emption | A resource cannot be forcibly taken from its holder |
| Circular wait | A holds what B wants while B holds what A wants (a cycle) |
A two-process, two-lock cycle shows it minimally:
| Step | Process A | Process B |
|---|---|---|
| 1 | lock Resource 1 | |
| 2 | lock Resource 2 | |
| 3 | wait for Resource 2 — blocked (B holds it) | |
| 4 | wait for Resource 1 — blocked (A holds it) |
Neither can advance. The famous illustration is the Dining Philosophers: five philosophers around a table, one fork between each pair; if every philosopher picks up their left fork at once, each then waits forever for a right fork their neighbour is holding. Because deadlock needs all four conditions, breaking any one prevents it:
| Strategy | Which condition it breaks |
|---|---|
| Acquire all resources in a fixed global order | circular wait (the cycle cannot form) |
| Timeout: if a lock is not obtained in time, release everything and retry | hold and wait |
| Use atomic/lock-free operations where possible | mutual exclusion (no lock to deadlock on) |
| Detect a cycle and forcibly abort one task | no pre-emption |
Resource ordering is the most common and elegant fix: if every task always locks Resource 1 before Resource 2, the cyclic wait that deadlock depends on can never arise. It is worth stressing that deadlock is a silent failure — the program does not crash, it simply hangs forever — which makes it harder to spot than a race condition that produces a visibly wrong value, and is why prevention by design is so much preferred to detection after the fact.
To make critical sections safe, concurrent systems use synchronisation mechanisms that enforce one-at-a-time access:
| Mechanism | What it does |
|---|---|
| Mutex (mutual exclusion lock) | A lock allowing exactly one task into the critical section at a time |
| Semaphore | A counter permitting up to N tasks to access a limited pool of resources |
| Monitor | An object that bundles data with locking, so access is automatically mutually exclusive |
| Atomic operation | A single indivisible operation that cannot be interrupted partway |
Subscribe to continue reading
Get full access to this lesson and all 10 lessons in this course.