openskills.info

Concurrency Fundamentals

itComputer fundamentals

Concurrency Fundamentals

Concurrency lets a program manage several independent activities whose lifetimes overlap. Parallelism means computations execute at the same instant. A concurrent program can run on one processor by taking turns. A parallel program needs execution capacity for work to run simultaneously.

You meet concurrency whenever software must remain responsive while waiting, serve many requests, process background jobs, or use several processor cores. The benefit is not automatic speed. Concurrency gives you a way to structure overlapping work. Parallelism can reduce elapsed time when the work and hardware allow it.

The central difficulty is order. A single sequence of instructions has one obvious next step. Concurrent tasks can interleave in several valid orders. Your design must remain correct for every order the runtime permits, not only the order you saw during one test.

Units of concurrent work

A process is a running program with its own operating-system resources and address space. A thread is an execution path within a process. Threads in one process commonly share memory. A runtime may also offer lighter units such as tasks, futures, coroutines, or goroutines.

An asynchronous task can pause while it waits and let other tasks use the same thread. This cooperative style works well for workloads with many waits, such as network services. A task must reach an await or another yield point before peer tasks can run on that event-loop thread.

A thread can be preempted by the operating system. The scheduler may interrupt it between operations and run another thread. That difference changes where interleavings can occur, but it does not remove the need to reason about ownership, ordering, and completion.

Shared state creates coordination work

Shared mutable state is data that more than one concurrent task can read or change. Suppose two tasks increment one counter. Each increment contains a read, a calculation, and a write. Both tasks can read the same old value and then write the same new value. One increment disappears.

That bug is a data race when conflicting accesses to the same memory location are not ordered by synchronization. A broader race condition occurs when correctness depends on timing or relative order. Not every race condition is a language-level data race.

A critical section is code that accesses a shared invariant and must not overlap with conflicting code. A mutex, short for mutual exclusion, lets one task at a time hold the lock that protects that section. The lock does two jobs: it excludes conflicting access and establishes ordering required by the language's memory model.

Keep the protected state and its lock conceptually together. State the invariant that the lock protects. Keep the critical section small enough to understand, but do not split one invariant across unlocked gaps.

Visibility needs a memory model

Source order in one thread does not by itself tell another thread when writes become visible. Compilers, processors, and runtimes may use reorderings that preserve the rules of the language.

A memory model defines which values a read may observe. A happens-before relationship is an ordering guarantee between actions. Correct synchronization creates these relationships. For example, releasing a lock before another task acquires that lock can make earlier writes visible to the later task under the relevant language rules.

Atomic operations perform a specified indivisible memory action and provide documented ordering guarantees. They are useful for small state transitions such as counters and flags. They do not automatically protect a multi-field invariant. Use the highest-level primitive that expresses the rule you need.

Communication can replace shared mutation

Message passing sends data through a channel, queue, or mailbox. It can reduce shared mutation by giving one task responsibility for a piece of state. Other tasks send requests or results instead of editing that state directly.

Message passing still needs design decisions. You must choose who owns the data, what ordering the channel provides, what happens when the channel fills, and how closure or failure is reported. An unbounded queue can move a problem from lock contention to memory growth.

Structured concurrency treats child tasks as part of a bounded parent operation. The parent waits for them, propagates failure, and cancels unfinished work when the operation ends. Even when a library uses a different name, this ownership model helps prevent abandoned background work.

Liveness failures

Correct values are not enough. Concurrent work must also make progress.

Deadlock means tasks wait in a cycle and none can continue. The classic case is one task holding lock A while waiting for lock B, as another holds B while waiting for A. A consistent global lock order prevents that specific pattern.

Livelock means tasks keep reacting to one another but make no useful progress. Starvation means one task repeatedly loses access to a resource. Contention means tasks compete for a resource and spend time waiting rather than doing useful work.

Timeouts can bound a wait, but they do not prove the shared protocol is correct. Cancellation also needs a defined path. A task that receives cancellation must release resources and leave shared state valid.

Choose a model from the workload

For I/O-bound work, asynchronous tasks or a bounded thread pool can overlap waits without creating one operating-system thread per operation. For CPU-bound work, parallel workers can use several cores when tasks are independent enough. More workers eventually add scheduling, communication, and contention costs.

Start with the simplest ownership model:

  1. Prefer immutable data and task-local state.
  2. Transfer ownership through messages when one task can own the state.
  3. Protect genuinely shared invariants with a clear synchronization primitive.
  4. Bound queues, workers, retries, and waits.
  5. Define completion, failure, and cancellation before tuning throughput.

Testing and diagnosis

Concurrency bugs can disappear when logging changes timing. Repeat tests, vary scheduling pressure, and run race-detection tools offered by your language. A clean race-detector run covers only the execution paths that ran. It is evidence, not proof.

Test invariants and outcomes instead of requiring one incidental event order. Add tests for cancellation, queue saturation, partial failure, and shutdown. Use traces, thread dumps, task dumps, and contention profiles to inspect waiting and ownership.

Where this course stops

This course gives you the mental model and vocabulary for concurrent software. It does not teach one language's API or advanced lock-free algorithms. Continue with your language's memory model, its high-level concurrency library, and its diagnostic tools. Then practice designing bounded worker systems before moving to atomics and lock-free structures.