Threads, Race Conditions, and Locks

Learn how threads let one program do several things at once, and why sharing data between them is risky. Create and join threads with POSIX threads in C, trace how race conditions lose updates, and protect critical sections with mutexes. See how locks are built from atomic hardware instructions such as test-and-set and compare-and-swap, why operating systems put waiting threads to sleep, and how to design counters, lists, queues and hash tables that stay correct and fast with many threads.

IntermediateLesson 5 of 9 in Operating Systems Course

What you will learn

  1. Describe how the threads of one process share an address space while each keeps its own execution state, and write C programs that create threads, pass them data and wait for them to finish.
    • Compare threads with processes
      Explain what a thread owns, what it shares with the other threads of its process, why programs use threads and how switching between threads differs from switching between processes.
    • Create, pass data to and join threads with pthreads
      Use the POSIX thread API in C to start threads, give each one its arguments, collect their results and make sure the program waits for them before it ends.
  2. Trace how interleaved machine instructions on shared data produce wrong results, name race conditions and critical sections in code, and state what a correct critical-section solution must guarantee.
    • Trace how unprotected updates lose increments
      Break a shared update into its machine steps, trace interleavings that lose updates and explain why such bugs are rare in testing yet still real.
    • Identify race conditions, critical sections and atomic operations
      Define the core vocabulary of concurrency, recognize read-modify-write and check-then-act races in code, and state the requirements a correct solution to the critical-section problem must meet.
  3. Use POSIX mutexes to make critical sections atomic, decide what each lock protects and how much code it covers, and avoid the mistakes that silently break locking.
    • Lock and unlock a POSIX mutex around critical sections
      Explain what a lock guarantees, initialize and use a pthread mutex correctly, and fix the shared-counter and bank-account races with it.
    • Choose lock granularity and evaluate locks
      Decide how much data each lock guards and how much code it covers, and judge a lock by correctness, fairness and performance.
    • Avoid common locking mistakes
      Recognize the typical errors that leave data unprotected or leave threads waiting forever, and correct each one.
  4. Show why ordinary loads and stores are not enough for a correct lock on modern hardware, build spin locks from atomic instructions, make them fair, and explain how operating-system support avoids wasted spinning.
    • Explain why simple lock attempts fail
      Analyze three early approaches to mutual exclusion, disabling interrupts, a plain flag and Peterson's algorithm, and explain where each one breaks down.
    • Build a spin lock with atomic instructions
      Describe test-and-set, compare-and-swap, load-linked/store-conditional and fetch-and-add, build a spin lock from them and evaluate it.
    • Make spin locks fair with a ticket lock
      Build a ticket lock from fetch-and-add, trace it and explain how it guarantees that every waiting thread gets its turn.
    • Avoid wasted spinning with yielding, sleeping and two-phase locks
      Explain how yielding, sleeping on a queue, futexes and two-phase locks reduce wasted CPU time, decide when spinning is worth it, and recognize priority inversion.
  5. Make counters, linked lists, queues and hash tables safe for many threads with locks, and judge each design by its correctness and by how well it scales as threads are added.
    • Build counters that scale
      Protect a counter with one lock, explain why it scales poorly, and build an approximate counter with per-core counters and a threshold.
    • Make linked lists thread-safe
      Protect a linked list with one lock while keeping the critical sections small and every path unlocking, and judge when per-node locking helps.
    • Design concurrent queues and hash tables
      Build a two-lock queue with a dummy node and a hash table with a lock per bucket, and follow a measured approach to finer-grained locking.

Threads and Locks study guide

5 chapters

1

Threads and their shared memory

A process gives a program an address space; threads give it several paths of execution within that space. Start by separating the state each thread carries from the objects they can all reach. Then use the POSIX thread interface to start workers and collect their results. The main design questions are who owns each argument, how long its memory stays alive, and when the creating thread may safely read the answer.

1.1 One address space, several paths of execution

A thread is one sequence of execution inside a process. Several threads can work on the same program data, but each needs its own place in the code and its own unfinished function calls.

What is shared and what belongs to a thread

Threads in one process share code, global and static variables, heap objects, open file descriptors, and the process ID. Each thread has its own program counter, registers, stack, and thread ID. A global int total denotes one shared object. An ordinary local int i in the worker function denotes a different object in each call.

A three-thread process has three stacks at different addresses in the same address space. A stack is not a protected compartment: another thread can reach a local variable if it receives its address. That sharing is valid only while the variable exists. A pointer into a function's finished stack frame cannot keep the variable alive. Large local arrays and deep recursion can also exhaust a thread's stack.

Switching between threads

The operating system saves each thread's execution state in a thread control block, or TCB. When it switches from T1 to T2, it saves T1's registers and restores T2's registers. T1's unfinished calculation stays in its saved registers until it runs again.

For threads in the same process, the address space stays the same during the switch. The page tables need not change, which usually makes this switch cheaper than switching between processes. Shared memory also means shared failures: an invalid pointer write can damage another thread's data, and a fault that terminates the process ends every thread.

Why use several threads?

Parallelism means executing at the same instant on different CPU cores. Four independent parts of a computation can approach a fourfold speedup on four cores, if shared work and coordination do not become bottlenecks. Threads also let a program overlap waiting with work: while one waits for a disk read or network reply, another can use the CPU.

Concurrency means that executions make progress over overlapping periods. On one core, the scheduler switches between threads, so they are concurrent without running in parallel. A single core still allows concurrency bugs: a switch can interrupt a shared update between its machine instructions.

Who schedules the threads?

Linux and Windows use a one-to-one model: each ordinary application thread corresponds to a kernel-scheduled thread. Different threads can run on different cores, and a blocking call blocks its calling thread. A many-to-one library schedules several user threads on one kernel thread. Its switches can be cheap, but it cannot run those threads on several cores, and a blocking system call can stop the whole group.

Creating A before B does not promise that A runs first. If A prints "A" and B prints "B", either "AB" or "BA" can appear. Creation order establishes when a thread becomes available to run; the scheduler chooses its execution time.

Key Takeaways

  • Threads share the process address space, but each has its own registers, program counter, and stack.
  • A pointer can make a stack variable shared; its lifetime still ends with its owning function.
  • Concurrency is possible on one core; parallel execution needs several cores.
  • The scheduler chooses which runnable thread executes next.
  • One-to-one threading supports parallel execution and per-thread blocking.

1.2 Start workers and collect their results

The POSIX threads interface, or pthreads, starts a C function in another thread. Include <pthread.h> and build with a command such as gcc -pthread -o sum sum.c. The -pthread option supplies the compiler and linker settings for threaded code.

Create a thread with stable arguments

The call pthread_create(&t, NULL, worker, arg) stores a thread ID in t and starts worker(arg). The NULL attribute pointer requests defaults. A worker has the signature void *worker(void *arg): the argument and return value are pointers. A struct lets that one pointer carry several fields.

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

struct range { int lo, hi; long result; };

void *sum_range(void *arg) {
    struct range *r = arg;
    long sum = 0;
    for (int n = r->lo; n <= r->hi; n++)
        sum += n;
    r->result = sum;
    return NULL;
}

The creator prepares two structs, {1, 50, 0} and {51, 100, 0}, and passes a different address to each worker. Keep both structs alive until the workers finish. Each worker accumulates in its own local sum and writes only its own result field.

pthread_create returns 0 on success and an error number on failure. Check that returned number directly; it does not set errno to report the failure. Only a successful creation gives a thread to join.

Join before reading results

After successfully creating both workers, main calls pthread_join(t[0], NULL) and pthread_join(t[1], NULL). A join blocks until its target ends. Successful joining also makes that worker's writes visible to the joining thread. Main can then read 1,275 and 3,775 and add them to obtain 5,050 without a mutex: the workers wrote separate objects, and main waited before reading them.

Starting both threads before joining allows their work to overlap. Creating one worker and immediately joining it before creating the next makes the workers run one after the other.

Here is the driver. It exits on a pthread error, so it never uses an ID from a failed creation:

static void require_success(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

int main(void) {
    struct range jobs[2] = {{1, 50, 0}, {51, 100, 0}};
    pthread_t threads[2];
    for (int i = 0; i < 2; i++)
        require_success(pthread_create(&threads[i], NULL,
                                      sum_range, &jobs[i]));
    for (int i = 0; i < 2; i++)
        require_success(pthread_join(threads[i], NULL));
    printf("%ld\n", jobs[0].result + jobs[1].result);
    return 0;
}

To collect a returned pointer, pass its receiving address: void *answer; pthread_join(t, &answer);. The pointer must refer to storage that outlives the worker, such as allocated heap memory or a creator-owned struct. Returning &local_result from the worker is invalid because its local variable ends when the function returns.

Avoid sharing a changing loop variable

Passing &i to every worker in a creation loop passes one address repeatedly. Workers may read it after the loop has changed it, so several can receive the same apparent ID. Use an array instead: assign ids[i] = i, pass &ids[i], and keep the array alive and unchanged until all joins complete.

End threads and processes deliberately

Returning from main, or calling exit() in any thread, ends the whole process and can cut workers off. Main normally joins workers before returning. Calling pthread_exit() ends only its calling thread; other threads can continue.

A detached thread releases its thread resources automatically when it ends and cannot be joined. Arrange for each joinable thread to be joined once or detached. Joining waits for completion; it does not protect two workers that update the same object while both are running.

Key Takeaways

  • Check pthread return codes directly; join only successfully created threads.
  • Pass distinct, stable argument objects and keep them alive through worker completion.
  • Join before reading a worker result that it may still be writing.
  • Return pointers only to objects that outlive the worker.
  • Detach a thread when its resources should be reclaimed without a join.
  • Returning from main ends the process, including unfinished workers.
2

Find the shared steps that can interfere

A source statement can hide several machine operations. Following those operations in order makes a concurrency failure visible: one thread reads, another changes the object, and the first stores an answer based on its stale read. Use traces to find the shared sequence that must stay together. Then distinguish a data race from a larger timing error, and judge a solution by exclusion, progress, and waiting.

2.1 Follow a lost update instruction by instruction

The statement counter++ looks small, but an ordinary shared integer increment is not an indivisible operation. It usually loads a value, adds in a register, and stores the result. Another thread can run between those steps.

A trace that loses one increment

Start with counter equal to 50. Treat load, add, and store as separate machine steps:

StepRunning threadActionShared counter
1T1Load 50 and add 1 in its register50
2T2Load 50 and add 1 in its register50
3T2Store its register value, 5151
4T1Store its register value, 5151

Two increments should have produced 52. T1 instead overwrites T2's result with a value computed from the older 50. This is a lost update.

A context switch preserves T1's registers in its TCB. When T1 resumes, its register still holds 51; it does not reload the shared counter simply because T2 ran. The error lies in the unprotected sequence, not in the scheduler restoring a wrong register.

Compare with a serial execution

If T1 completes all three steps before T2 loads, T1 stores 51 and T2 then stores 52. Reversing the whole-thread order also gives 52. The interleaved result 51 cannot arise from either serial order. To find this kind of failure, mark a read, place another thread's write after it, and resume the first thread's stale computation.

This trace is a machine-level model of the bug. In actual C, unsynchronized conflicting accesses to an ordinary shared integer form a data race, which gives undefined behavior. C does not limit that program to the results of a simple instruction-interleaving model.

Why passing tests is weak evidence

Two threads performing 10,000,000 increments each should produce 20,000,000. Unprotected experimental runs can print different smaller totals, such as 19,345,221 and 19,221,041 in published examples. A good-looking run only means its observed outcome happened to be acceptable.

The vulnerable window may be only a few instructions wide. Different core counts, compiler choices, or system load can expose a schedule that thousands of earlier runs missed. For two threads performing one modeled increment each from zero, the trace model permits 1 or 2. With at least two increments per thread, rare schedules can leave a total as low as 2 even as the number of intended increments grows.

Fix the operation, not its appearance

Declaring the integer volatile does not make its load, addition, and store indivisible and does not synchronize threads. Writing all the work on one line changes nothing. Even an x86 instruction that adds directly to memory needs the appropriate atomic form to coordinate across cores.

Protect the complete read-modify-write with a shared mutex, or use a C11 atomic read-modify-write such as atomic_fetch_add. The same need exists on one core: a timer-driven switch can separate the load from the store even without parallel execution.

Key Takeaways

  • An ordinary increment can be a load, an addition, and a store.
  • A stale store can overwrite another thread’s completed update.
  • Context switches preserve each thread’s private registers.
  • Volatile supplies neither atomicity nor thread synchronization.
  • A C data race has undefined behavior; a trace illustrates the failure without bounding it.

2.2 Keep a decision and its update together

A concurrency error can involve one memory access or a whole business rule. Identify both the shared objects and the operation that must appear indivisible to other threads.

Race condition or data race?

A race condition occurs when correctness depends on relative timing. A data race occurs when different threads access the same memory location without synchronization, at least one writes it, and at least one access is non-atomic. C11 and C++11 assign undefined behavior to a data race.

Atomic accesses can avoid a data race while a larger operation still has a race condition. For example, individually protected reads and writes do not keep a decision valid between them. Two threads can each read a sufficient balance, release the lock, and later withdraw using that outdated decision.

Work through a withdrawal

A balance starts at 100. Two threads each request 80. The rule is to accept a withdrawal only when the balance covers it.

  1. T1 checks 100 and decides its withdrawal can succeed.
  2. T2 also checks 100 and makes the same decision.
  3. T1 subtracts 80, leaving 20.
  4. T2 acts on its earlier decision and subtracts 80 from 20, leaving −60.

Protecting only the subtraction does not fix that sequence. Protect the check and the subtraction with the same lock, held continuously. Then one thread leaves 20, and the next checks 20 and refuses. A different unprotected schedule can lose a subtraction and leave 20 after both withdrawals were paid, which is also wrong.

Find the critical section

A critical section is the shared-data region that must exclude competing access. A read-modify-write sequence first reads data, computes a new value, and writes it back. A check-then-act sequence decides whether an action is allowed and then performs it. List operations, lazy initialization, and file creation can contain these patterns too.

A lock gives mutual exclusion: at most one participating thread is inside the protected region at a time. An atomic operation appears to other participating threads as one complete operation; they cannot observe a half-finished state. A mutex provides that view only for code that follows the same locking agreement.

Not every object needs a lock. Data accessed by one thread stays private unless its address is shared. Data fully initialized before workers start and only read thereafter can be shared without locking. Judge who can reach an object and whether writes overlap its use, not just where it was declared.

What a correct solution promises
  • Mutual exclusion: competing threads cannot enter together.
  • Progress: when the section is free and threads want it, choosing an entrant cannot be postponed indefinitely by threads that are not trying to enter.
  • Bounded waiting: after a request, a limit exists on how many competing entries can precede it.

C11 atomic_fetch_add(&count, 1) performs one atomic update on an atomic_int. Atomicity is guaranteed even on a platform that implements the operation with an internal library lock. Making two account fields individually atomic still does not turn a transfer between them into one atomic transaction; the relationship between the fields needs protection too.

Key Takeaways

  • Data races concern conflicting unsynchronized memory accesses; race conditions concern timing-dependent correctness.
  • A check and the action that relies on it belong in one critical section.
  • Mutual exclusion, progress, and bounded waiting are separate requirements.
  • Separate atomic accesses do not make a multi-step operation atomic.
3

Protect shared operations with mutexes

A lock works through a shared agreement: every operation on protected data follows the same rule. Learn the POSIX mutex lifecycle, then decide where a critical section begins and ends. Short sections reduce waiting, but a check must stay with the action it authorizes. Reading code for missing unlocks, mismatched locks, and unsafe lifetimes turns that agreement into something you can review and maintain.

3.1 Use a mutex from initialization to release

A mutex is a lock that lets one thread at a time own a protected region. A thread acquires it before touching guarded data and releases it afterward. If another thread holds it, a blocking lock call waits.

Initialize the shared mutex

Use pthread_mutex_t from <pthread.h>. A static mutex can start with pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;. A dynamically created object can initialize its field with pthread_mutex_init(&obj->lock, NULL). Initialization must finish before any thread uses the mutex.

Here is a counter operation using a statically initialized shared mutex. The helper checks pthread return values, which are error numbers directly:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

static pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
static long counter = 0;  // guarded by m

static void check(int rc) {
    if (rc != 0) {
        fprintf(stderr, "pthread error: %d\n", rc);
        exit(EXIT_FAILURE);
    }
}

void increment(void) {
    check(pthread_mutex_lock(&m));
    counter = counter + 1;
    check(pthread_mutex_unlock(&m));
}

Every concurrent read or update of counter must follow this same rule. A separate function using a different mutex would not coordinate with increment.

What happens while the mutex is held?

T1 locks m, loads the counter, computes, and stores. If T2 calls pthread_mutex_lock(&m) during that interval, it cannot pass the call until it acquires ownership after T1 releases. T1 can still be preempted: a mutex prevents competing entry, not context switches.

For the balance rule, hold the mutex before evaluating balance >= amount. Subtract only if it passes, record success or failure, and then unlock. A balance of 100 with two requests for 80 allows exactly one success and leaves 20.

Choose how to wait

pthread_mutex_trylock(&m) returns immediately: 0 means the caller acquired the mutex; EBUSY means it did not because the mutex was held. The busy path can do other work and must not access the guarded data. pthread_mutex_timedlock waits until an absolute deadline. These interfaces still require checking return codes and releasing a lock only after a successful acquisition.

Respect ownership and lifetime

The owning thread releases a mutex. A normal nonrecursive mutex blocks if its owner tries to lock it again. An error-checking mutex reports mistakes such as relocking with EDEADLK or a non-owner unlock with EPERM. A recursive mutex counts repeated acquisitions by its owner and requires the same number of releases. Do not depend on a default mutex to diagnose misuse: its behavior depends on the implementation's mapped type and the operation.

When all threads have stopped using the mutex and it is unlocked, call pthread_mutex_destroy. Destroying or freeing an object while another thread can still lock it breaks the mutex's lifetime. Passing the mutex by pointer preserves its identity; copying it does not create another valid handle to the same lock.

Key Takeaways

  • Initialize a mutex before making it available to workers.
  • Acquire the same shared mutex before every competing access to its guarded data.
  • A held mutex prevents competing entry while its owner can still be preempted.
  • Trylock returning EBUSY means no ownership was acquired.
  • Keep the mutex alive until no thread can use it again.
  • Error-checking mutexes report ownership mistakes; recursive mutexes count nested acquisitions.

3.2 Choose what one lock protects

Lock granularity is the amount of shared state covered by one lock. It determines which operations must wait for one another, even when they run on different cores.

One large lock or several smaller locks?

A coarse-grained design puts one mutex around a whole structure or subsystem. It has a simple protection rule, but unrelated operations serialize. A fine-grained design assigns separate mutexes to separate accounts, buckets, or regions. Threads can then work in different regions at once, at the cost of more lock operations and more complicated rules.

Put the guard beside the state it protects:

struct account {
    pthread_mutex_t lock;
    long balance;
    unsigned id;
};

An operation on one account takes its mutex. A transfer between two distinct accounts needs both because it changes their relationship. Choose a fixed order, such as increasing account ID, and use it on every path. Otherwise one thread can hold A and wait for B while another holds B and waits for A.

A lock puts a ceiling on throughput

Suppose every operation holds a single global lock for 2 µs. The protected work can complete at most one operation per 2 µs:

maximum throughput=12×10−6=500,000 operations per second.

Adding cores cannot make two operations hold that same mutex simultaneously. Lock overhead and other work can lower the achieved rate further. The protected duration is therefore a useful limit to calculate before expecting speedup.

Shorten the section without breaking the operation

Prepare private work before acquiring the mutex. For a list insertion, allocate the new node and fill its key first. Acquire the list mutex only when reading the current head and linking the node. Allocation failure then occurs without a held lock.

Slow I/O, printing, or an unknown callback can keep every waiter stalled. Move such work outside when the data can remain valid there. Keep a decision and its dependent update together: shortening a withdrawal by unlocking between its balance check and subtraction destroys correctness.

Evaluate three different properties

Mutual exclusion asks whether the design really prevents conflicting entry. Fairness asks whether a waiting thread eventually gets a turn or can starve. Performance asks what the lock costs with one thread, several threads on one CPU, and threads on several CPUs. A lock can be correct and still unfair or slow.

Contention occurs when threads compete for an already-held lock. Under heavy contention, they also move shared lock and data cache lines between cores. One published counter experiment took about 0.03 s for one thread's million updates but over 5 s for two threads doing a million each; those are measurements of that setup, not universal timings.

For a scaling experiment, keep each thread's work fixed. Ideal scaling lets N threads on N cores do N times the work in the original time. Start with a simple correct structure, measure waiting and total time, and split a lock only where the additional concurrency pays for its added overhead.

Key Takeaways

  • Coarse locks simplify protection; finer locks allow independent regions to proceed together.
  • A single lock serializes its protected work and limits throughput.
  • Take multiple locks in one fixed order.
  • Judge exclusion, fairness, and performance separately.
  • Measure before replacing a simple lock with a more complicated design.

3.3 Review every path through the lock

A mutex can be implemented perfectly while a program still uses it incorrectly. Review the whole path through a function, including reads, errors, callbacks, and object destruction.

Make every exit release ownership

This function leaks the lock on its failure path:

pthread_mutex_lock(&m);
if (failed) return -1;
update_shared_state();
pthread_mutex_unlock(&m);

The returning thread may finish, but the mutex stays held. A later caller then waits for a release that will never happen. Use one cleanup path or place an unlock before every return. With checked pthread calls, the structure can be:

int rc = -1;
check(pthread_mutex_lock(&m));
if (!failed) {
    update_shared_state();
    rc = 0;
}
check(pthread_mutex_unlock(&m));
return rc;

Reviewing one release point is easier than reviewing many scattered ones. The important property is that every successfully acquired lock is released on every ordinary exit.

Follow the protection rule on reads too

A status function that "only reads" can race with an updater. It can also observe related fields at different stages of one update. If a list operation publishes a node before completing its fields, an unprotected reader may follow a pointer into unfinished data. Acquire the documented guard for each access whose correctness depends on it.

Using different mutexes for the same object does not help. If T1 takes lockA and T2 takes lockB, both acquisitions can succeed. A mutex declared as a fresh local inside every worker makes this error easy to miss: each thread locks its own object, so no shared exclusion exists.

Preserve both identity and operation boundaries

A pthread_mutex_t cannot be copied by assignment or by copying its containing struct and then used as a second mutex handle. Using the copy is undefined behavior. Pass a pointer to the original shared mutex.

Also check where releases occur. A function that locks to check a balance, unlocks, and later relocks to subtract has protected individual accesses but left the decision stale. Keep the complete check-then-act sequence under continuous ownership.

Avoid waiting on yourself

Calling unknown code while holding a mutex can unexpectedly reenter the same protected object. With a normal mutex, a second lock call by the owner blocks that owner. An error-checking mutex reports this mistake, while a recursive mutex allows nested ownership and needs matching unlocks. Select a type deliberately; do not use recursion to hide an unclear protection design.

Keep slow I/O and sleeps out of a shared critical section when possible. Before destroying the mutex or freeing its containing object, ensure no thread owns it or may access it again. Initialization, use, and destruction must have a clear order.

Use runtime tools to expose observed failures

ThreadSanitizer, enabled with -fsanitize=thread, and Valgrind's Helgrind can report data races and lock misuse in executions they observe. Combine their reports with a review of shared accesses and possible interleavings. A clean run establishes that the tool found no problem in those executions; an untested path still needs reasoning.

Key Takeaways

  • Release each successfully acquired mutex on every exit path.
  • Readers must follow the protection rule when writers can run concurrently.
  • A mutex is shared by address; separate or copied mutexes do not coordinate access.
  • Do not split a check from its action with an unlock.
  • Unknown callbacks can reenter code and attempt to acquire the held mutex.
  • A clean race-detector run covers observed executions, so review untested paths too.
4

How locks acquire, wait, and hand off

A lock must make its ownership decision indivisible and order the protected memory accesses. Ordinary flags do neither. Build from failed lock attempts to atomic exchange, then add fairness with ticket numbers. Finally consider the waiter: spinning, yielding, and sleeping have different costs, and sleeping needs a protocol that cannot lose a wakeup. Scheduling priority can change which waiting strategy makes progress.

4.1 Why checking a plain flag is not enough

A lock needs a reliable way to choose one owner. An ordinary read followed by an ordinary write leaves a gap in that decision, even if the protected work itself is very short.

Interrupt control has a narrow role

On one CPU, a kernel can briefly disable interrupts around a nonblocking critical section to prevent interrupt-driven preemption there. This relies on staying on the CPU without voluntarily blocking or switching. The kernel uses the technique for short internal operations.

It is not a general application mutex. Interrupt-control instructions are privileged; giving an application unrestricted use would let it monopolize the CPU. Disabling interrupts on one core does not stop other cores from accessing the same data. Long disabled intervals also delay interrupt handling and can lose events on some devices.

Trace the broken flag lock

Consider this C-like pseudocode:

while (flag == 1) { }
flag = 1;
// critical section
flag = 0;

Start with flag at 0. T1 tests it and leaves the loop, then pauses before the assignment. T2 tests the same 0, sets it to 1, and enters. T1 resumes, sets it to 1, and enters too. Both passed the test before either claimed ownership.

Making the read and write individually atomic would remove a data race on the flag, but it would still leave two separate operations in this protocol. The ownership claim needs an atomic read-modify-write or a more complete synchronization algorithm.

The empty loop is spin-waiting: repeated checking while consuming CPU time. On one CPU, a waiter that spins after preempting the owner cannot make the owner release sooner. It must eventually let the owner run.

Peterson's two-thread solution

Peterson's algorithm coordinates two threads with an interest flag per thread and a shared turn. Thread i performs:

flag[i] = 1;
turn = 1 - i;
while (flag[1 - i] == 1 && turn == 1 - i) { }
// critical section
flag[i] = 0;

It announces interest, gives the other thread priority in a tie, and waits only while the other is interested and has priority. Under sequential consistency, every thread's operations retain program order and all threads observe one common order. In that model, Peterson's algorithm supplies mutual exclusion, progress, and bounded waiting for two threads.

The memory order matters too

Modern processors and compilers may let memory operations become visible in an order different from the source. A store can remain buffered while a later load reads memory. Both threads can then see the other's interest flag as 0 and both enter. Plain shared C integers also introduce C data races.

A valid implementation needs suitable atomic operations and ordering, such as sequentially consistent C11 atomics for the flags and turn. volatile does not provide that synchronization. Practical locks combine an indivisible ownership operation with memory-order guarantees so protected accesses stay on the correct side of acquisition and release.

Key Takeaways

  • Separate testing and setting lets two threads claim the same free lock.
  • Disabling interrupts on one CPU does not exclude another core.
  • Peterson’s two-thread algorithm requires sequentially consistent behavior.
  • Atomic ownership and correct memory ordering are both necessary for a practical lock.

4.2 Use an atomic instruction to choose one owner

An atomic read-modify-write combines reading a location and updating it into one indivisible action. It closes the gap that lets two threads pass an ordinary flag test together.

Exchange the flag and inspect its old value

Test-and-set, or atomic exchange, writes a new value and returns the old one. With a flag initially 0, T1 exchanges in 1 and receives 0, so it enters. T2 exchanges in 1 and receives 1, so it waits. Until release writes 0, no other exchange can return 0.

Here is a C11 spin lock using the default sequentially consistent atomic operations:

#include <stdatomic.h>

static atomic_flag held = ATOMIC_FLAG_INIT;

void lock(void) {
    while (atomic_flag_test_and_set(&held)) { }
}

void unlock(void) {
    atomic_flag_clear(&held);
}

The ordering matters as well as the flag. Acquisition must keep subsequent critical-section accesses after the successful acquire; release must keep earlier critical-section accesses before the release. Clearing the flag must also be atomic; mixing ordinary stores with atomic accesses does not implement this handoff.

Compare before replacing

Compare-and-swap, or CAS, changes a value only when it equals an expected value. Suppose two threads read a counter at 7 and each wants to add 1. T2 successfully changes 7 to 8. T1's attempt to change 7 to 8 now fails, so T1 retries from 8 and changes it to 9. Retrying preserves both increments.

C11 atomic_compare_exchange_strong returns success or failure and updates the expected argument on failure. Some hardware-style pseudocode instead returns the old stored value. Check which convention a trace uses. On x86, memory xchg implements exchange, while lock cmpxchg supplies atomic compare-and-exchange.

Other hardware building blocks

Load-linked/store-conditional, or LL/SC, reads while establishing a reservation and then attempts a write. The conditional write fails if another write invalidated the reservation; it can also fail for other reasons, so it belongs in a retry loop. ARM's LDXR/STXR and RISC-V's lr/sc support this pattern.

Fetch-and-add adds atomically and returns the old value. C11 exposes it as atomic_fetch_add; x86 provides lock xadd. Returning the old value makes it useful for handing out unique ticket numbers.

C guarantees the atomic behavior of its atomic library operations. It does not require every atomic type to be lock-free; some operations use an internal library lock. atomic_flag has the lock-free guarantee, and atomic_is_lock_free checks other objects.

Correct does not mean fair or fast

A test-and-set lock gives mutual exclusion, but no arrival order: a thread can repeatedly lose the race after release and starve. On one CPU, four threads with 10 ms round-robin time slices can waste 30 ms spinning in three waiters before the preempted owner runs again. On several cores with a running owner and short sections, a short spin can avoid sleep-and-wake overhead.

Key Takeaways

  • Test-and-set returns the old flag while atomically writing the new one.
  • CAS retries after a stale expected value, preserving competing updates.
  • LL/SC code must handle conditional-store failure by retrying.
  • Fetch-and-add returns the old value as it performs the increment.
  • Lock acquisition and release must also order protected memory accesses.
  • A test-and-set spin lock gives exclusion but no starvation guarantee.

4.3 Hand out turns with a ticket lock

A test-and-set lock remembers whether it is held but not who has been waiting. A ticket lock adds an arrival order using an atomic ticket dispenser and a serving counter.

Take a number, then wait for it

Both counters start at 0. Each caller atomically increments ticket and keeps its old value as mine. It enters only when turn equals mine. The owner releases by increasing turn once.

#include <stdatomic.h>

static atomic_uint ticket = 0;
static atomic_uint turn = 0;

void lock(void) {
    unsigned mine = atomic_fetch_add(&ticket, 1);
    while (atomic_load(&turn) != mine) { }
}

void unlock(void) {
    atomic_fetch_add(&turn, 1);
}

These default C11 atomic operations supply both atomicity and sequentially consistent ordering. Each caller keeps mine in its own local variable. The shared ticket counter dispenses unique outstanding numbers.

Follow three arrivals

A takes ticket 0 and enters because turn is 0. B takes ticket 1 and waits. C takes ticket 2 and waits. The next unused ticket is now 3, while the current turn is still 0.

When A releases, turn becomes 1, so B enters. C still waits for 2. B's release advances turn to 2 and C enters. A newly arriving thread gets ticket 3 and cannot cut ahead of C.

This distinction matters when tracing: ticket records the next number to hand out; turn records the number being served. Taking a ticket changes only the dispenser. Release advances service.

What fairness guarantees

A caller waits behind the threads with earlier tickets. If every holder eventually releases and the threads ahead can run, later arrivals cannot overtake it. The number of competing critical sections ahead is bounded by the earlier outstanding tickets. This supplies bounded waiting and prevents starvation under those progress conditions.

The guarantee concerns entry order, not elapsed time. A holder that stalls for a long time can make every later caller wait a long time too. A ticket number cannot force the scheduler to run its owner.

Costs that ticket order leaves behind

Every waiter still spins. If the thread whose turn comes next is descheduled, later runnable threads wait even though no thread currently occupies the critical section. This is a scheduling cost of strict order.

Waiters also read one shared turn location. Each release changes the cache line they all watch, producing traffic across cores. Queue-based spin locks reduce that traffic by letting each waiter watch a separate local location; Linux moved from ticket spin locks to queued designs.

Real ticket counters use unsigned arithmetic. Equality checks can work across wrap-around when the number of outstanding tickets stays smaller than the counter's value range. Reusing a number while an earlier equal ticket is still outstanding would destroy uniqueness and exclusion.

Key Takeaways

  • Ticket allocation is atomic; a caller enters only when its number equals the current turn.
  • Release advances turn, while acquisition allocates from ticket.
  • Earlier ticket holders cannot be overtaken by later arrivals.
  • Fair order still leaves spinning, scheduling stalls, and shared-cache traffic.

4.4 Let waiters sleep without missing a release

A correct lock can waste most of a CPU on waiting. The right waiting strategy depends on whether the owner can run and how long it will hold the lock.

Yielding gives up a turn

A waiter can call yield() after a failed ownership attempt. Yielding moves it from running to ready so the scheduler can select another thread. It is still runnable, so it may soon be selected and fail again. With 100 threads on one CPU, up to 99 waiters can each run and yield before a preempted owner gets another turn. Yielding reduces spinning but supplies no fair entry order.

Blocking removes the waiter from runnable work

A sleeping lock puts an unsuccessful caller on a wait queue. It consumes no CPU while blocked. Unlock wakes a waiter so it can run again. A small internal spin guard can protect the flag and queue; that guard is held only for brief bookkeeping.

Separating "decide to sleep" from "actually sleep" creates a lost wakeup risk:

  1. A waiter queues itself and releases the guard.
  2. It is paused before calling park().
  3. The owner releases and calls unpark() for it.
  4. If that wakeup is discarded, the waiter later parks with nobody left to wake it.

Solaris's setpark() announces the intent to sleep. An intervening unpark then makes the later park return immediately. The protocol connects the decision to the actual blocking.

Linux uses an atomic compare-and-block

A futex combines a word in user memory with kernel support for waiting. futex_wait(addr, expected) blocks only if the word still equals the expected value; the check and blocking decision are atomic with respect to operations on that futex word. If an unlock already changed it, the caller does not go to sleep on the stale expectation.

futex_wake(addr, n) wakes up to n waiters. An uncontended lock can acquire and release with user-space atomic instructions, avoiding a system call. Linux's glibc pthread mutexes use futexes for contended waits. A wakeup makes a thread runnable; its locking protocol must still establish ownership before accessing the protected data.

Spin briefly, then sleep

A two-phase lock first spins briefly and then blocks if waiting continues. Spinning can be worthwhile when the owner is running on another core and the expected wait is shorter than sleeping and waking. Blocking is preferable for long waits or when runnable threads outnumber cores. On one CPU, a spinner cannot help a descheduled owner finish.

Scheduling priority can stop progress

In priority inversion, a high-priority thread waits for a mutex held by a low-priority thread while medium-priority work keeps the owner off the CPU. Sleeping the high-priority waiter avoids a spin, but does not remove that scheduling delay.

Priority inheritance temporarily raises the owner's priority to that of its highest-priority waiter, allowing it to finish and release. NASA's Mars Pathfinder experienced resets from priority inversion in 1997; enabling mutex priority inheritance corrected the problem. A high-priority spinner on a strictly priority-scheduled single CPU can be worse: it can prevent the low-priority owner from running at all.

Key Takeaways

  • Yielding keeps a waiter runnable; sleeping blocks it and consumes no CPU.
  • A correct sleep protocol closes the gap between deciding to sleep and actually blocking.
  • Futex wait checks its expected value atomically with the blocking decision.
  • A two-phase lock spins briefly before sleeping.
  • Priority inheritance helps a low-priority owner release a lock needed by higher-priority work.
5

Build shared structures that stay correct

A structure is thread-safe when concurrent calls obey its specification across their possible interleavings. Begin with one mutex and an explicit rule for every shared field. Then examine where operations genuinely need to exclude each other: counter updates, list traversal, opposite queue ends, and separate hash buckets have different needs. Each refinement must preserve safe publication and memory lifetime while earning its extra complexity in measurements.

5.1 Trade counter precision for fewer shared updates

A shared counter is easy to protect with one mutex. Its increment, decrement, and get operations each acquire the same lock, act, and release. It is exact, but all updates pass through one shared place.

Why the exact counter becomes a bottleneck

On several cores, a mutex serializes the counter operations. The cores also transfer ownership of the lock and counter cache lines. Adding workers can increase waiting faster than it increases useful work. Replacing the mutex with atomic_fetch_add removes explicit lock overhead on common processors, but every update still writes the same shared location.

An approximate counter, also called a sloppy counter, spreads updates over local counters and transfers them to a global counter in batches. Its reported global value deliberately omits recent local increments.

Build the local-to-global update

Keep one counter and mutex per core, a global counter with its own mutex, and a threshold S. For each increment:

  1. Choose the local slot for this update and acquire its mutex.
  2. Add 1 to the local count.
  3. If it reaches S, acquire the global mutex while still holding the local one.
  4. Add that local count to the global count, reset the local count to 0, and release the global mutex.
  5. Release the local mutex.

Keep the chosen slot stable throughout the update. Threads using different slots usually touch different locks, and acquire the global mutex only at a transfer. Reading the global count also follows its locking rule.

Follow a batch transfer

With four cores and S equal to 5, suppose their update totals are 7, 3, 4, and 0. Core 1 transfers 5 on its fifth increment and leaves 2 local increments afterward. The final local counts are 2, 3, 4, and 0. The global count is 5, and the true total is 14: nine increments remain local.

After completed unit-increment updates, each local count lies from 0 through S − 1. With P cores, the global total can lag by at most:

P(S−1).

For four cores and S equal to 5, the bound is 16. This bound concerns completed updates and their remaining local counts; do not apply it halfway through a transfer.

Choose the threshold for the job

A smaller threshold improves freshness but requires more global acquisitions. With S equal to 1, every increment transfers globally, adding local-lock overhead without avoiding the global bottleneck. A larger threshold improves update scaling but increases possible lag. In one published experiment, S equal to 1024 let four threads perform four million updates in nearly the time one thread needed for one million.

An exact snapshot must hold all local mutexes in a fixed order and then the global mutex before adding the counts. That ordering agrees with updates taking a local lock before the global lock. The snapshot stops concurrent transfers while it reads; reading each field separately could double-count or miss a transfer.

Approximate counters suit statistics that tolerate delayed visibility. A reference count that determines when memory can be freed needs an exact method: an underestimated count could free an object that is still in use.

Key Takeaways

  • A single atomic counter can still be a shared-cache bottleneck.
  • Approximate counters batch local increments into a guarded global counter.
  • After completed unit updates, the maximum lag is P(S − 1).
  • Larger transfer thresholds reduce global contention while increasing possible lag.
  • An exact snapshot holds all relevant locks in an order consistent with updates.

5.2 Publish and traverse linked-list nodes safely

A shared linked list needs to preserve both its links and its nodes' lifetimes. One mutex around each operation is a useful first design because every shared traversal and modification follows a single rule.

How two insertions lose a node

Suppose the list begins at A. T1 prepares node X and T2 prepares node Y. Without a shared lock, both can read head == A, assign their new node's next to A, and then store their own node into head. The later head assignment hides the first inserted node. That node becomes unreachable, losing an insertion and leaking its memory.

The protected sequence must include reading the current head, setting the new link, and publishing the new head. A lock only around the final assignment would allow both threads to build links from the same stale head.

Prepare private data before acquiring the lock

A new node remains private until it is linked into the shared list. Allocation and key initialization can happen before acquisition. Using the checked pthread helper from earlier, insertion can be written:

int insert(list_t *list, int key) {
    node_t *node = malloc(sizeof(*node));
    if (node == NULL) return -1;
    node->key = key;

    check(pthread_mutex_lock(&list->lock));
    node->next = list->head;
    list->head = node;
    check(pthread_mutex_unlock(&list->lock));
    return 0;
}

Allocation failure returns before any mutex is held. The shared section stays small while the check of head and the link update stay together. Thread-safe allocation does not make the list itself thread-safe; its links still need their own protection.

Readers participate in the same rule

A lookup takes the list mutex before reading head and retains it during traversal. This coordinates with insertion and removal. Set a result variable before the loop, update it on a match, and break to one unlock point before returning. A successful early return inside the locked loop would otherwise need its own release.

Under this design, removal holds the same mutex while unlinking a node. Other traversals cannot be in that node because they also need the mutex. Free it only when no other operation can still reach it. If an API lets a caller keep a node pointer after unlocking, that extra use needs its own lifetime agreement; the list lock alone no longer covers it.

Decide whether per-node locks pay off

Hand-over-hand locking, or lock coupling, acquires the next node's lock before releasing the current node's lock. That overlap preserves a protected path as traversal advances. Multiple operations can then occupy different regions of the list.

The extra concurrency has a price: every visited node adds lock and unlock operations, even when no other thread is nearby. A traversal-heavy workload can become slower than the single-mutex version. Locking groups of nodes is a middle ground. Compare the more complicated design with the simple one under the actual workload, including failed lookups and removal paths.

Key Takeaways

  • Protect the old-head read and the new-head publication together.
  • Allocate and initialize private node data before locking the list.
  • Lookup and removal follow the same guard as insertion.
  • Acquire the next node’s lock before releasing the current one in lock coupling.

5.3 Separate queue ends and hash buckets

One mutex per structure is a sound starting point, but it can serialize operations that touch different regions. Two useful refinements are a lock per queue end and a lock per hash bucket.

Keep a dummy at the queue head

A two-lock queue starts with one dummy node D and head == tail == D. D holds no current queue item. The first real item, if one exists, is at head->next. A head mutex serializes dequeues; a tail mutex serializes enqueues. One enqueue and one dequeue can therefore proceed together.

Enqueue prepares a new node's value and null next link privately. Under the tail mutex, it publishes that node through the old tail's next field and moves tail to the new node. Dequeue uses the head mutex to read the old dummy's next link. If it is null, it unlocks and reports empty. Otherwise it copies the first real value, moves head to that node, unlocks, and frees the old dummy.

The node that contained the returned value becomes the next dummy. Freeing that node immediately would leave head dangling.

Trace the moving dummy

Start with D. Enqueue 5 creates N5 and links D -> N5, with tail == N5. Enqueue 8 adds N8, producing D -> N5 -> N8. A dequeue returns 5, moves head to N5, and frees D. N5 is now the dummy; only 8 remains a queued value. After another dequeue, N8 becomes the dummy and the queue is empty.

The two ends still share a link

When the queue is empty, enqueue can write the current dummy's next while dequeue reads it under a different mutex. The two mutexes alone do not synchronize those accesses. In C11, declare next as an atomic pointer and publish/load it with appropriate ordering; default sequentially consistent atomic stores and loads suffice. Publishing must make the prepared node value visible before dequeue reads it.

Waiting for an item requires a signaling mechanism in addition to locking.

Hash by key, then lock the bucket

A fixed-size hash table can use a thread-safe list in each bucket, each with its own mutex. With 101 buckets and bucket = key % 101, keys 7 and 50 use different locks. Keys 7 and 108 both map to bucket 7 and must serialize there. Collisions affect contention as well as lookup length.

All accesses to a bucket's list follow that bucket's locking rule, including reads. Resizing changes the bucket mapping and moves entries, so this simple design needs exclusive access across the table during resizing. A bucket mutex alone cannot protect a change that affects every bucket.

Refine only after finding the bottleneck

Begin with a shared-state map: which fields are shared, which mutex or atomic operation protects each, and when nodes become reachable or can be freed. Check every return and allocation-failure path. Reason through interleavings, test under load with a race detector, and measure contention before adding finer locks.

Key Takeaways

  • A dummy lets queue head and tail operations use separate locks.
  • A successful dequeue frees the old dummy and turns the value node into the new dummy.
  • The shared next link needs atomic publication and reading in a C11 two-lock queue.
  • Separate bucket locks allow operations in different buckets to overlap.
  • Hash-table resizing requires coordination across the whole table.
  • Refine a correct design using measured contention and checked memory lifetimes.

Threads and Locks examples

5 Walkthroughs

1

Follow a worker from creation to completion

14 steps
Separate shared memory from thread state, pass stable arguments, and collect results after joining.
  1. 1
    IntroductionStart with two kinds of state

    A program can have several threads moving through its code. Each execution needs its own current instruction and unfinished calculations, while the threads can all reach the same program objects.

    Follow those two kinds of state first. Then build a two-worker sum and decide when main may read the results. The safety of that design depends on object lifetimes and completion, not on guessing which worker runs first.

  2. 2
    ExplanationGive each execution its own place

    A thread has its own program counter, registers, stack, and thread ID. Its stack stores its function-call frames and ordinary local variables. The operating system saves execution state in a TCB, or thread control block.

    When the scheduler switches threads, it restores the new thread’s state. A calculation in the first thread’s register stays unfinished until that thread resumes.

  3. 3
    ExampleFind the shared objects

    Suppose total is a global variable and each worker declares a local i. All workers reach one shared total, but each function call has its own i.

    The threads also share the heap and open file descriptors. Their stacks sit at different addresses inside that same process address space. Passing the address of one worker’s local variable lets another thread reach it; separate stacks do not enforce isolation.

  4. 4
    ConnectionOverlap work, even on one core

    On one CPU core, the scheduler interleaves threads: they make concurrent progress, but do not execute simultaneously. On several cores they can execute in parallel.

    Threads can speed independent computation or overlap I/O waiting with useful work. A same-process switch keeps the address space and page tables, so it usually costs less than a process switch. Shared memory also lets one invalid write damage other threads’ data.

  5. 5
    ExplanationSee what the kernel can schedule

    In a one-to-one model, each application thread is a kernel-scheduled thread. The scheduler can put them on different cores, and a blocking system call stops its calling thread. Linux and Windows use this model for ordinary threads.

    A many-to-one library runs user threads on one kernel thread. Those user threads cannot execute on several cores, and one blocking system call can stop their group.

  6. 6
    ChallengeDoes an extra core create the bug?

    A worker pauses after loading shared data, and another worker changes it before the first resumes. Could that happen on one core?

    ---reveal---

    Yes. The scheduler can switch between the load and the later use of the value. Parallel execution can expose more overlap, but concurrency alone permits this schedule. A claim that a one-core run is safe must account for switches between machine instructions.

  7. 7
    ExplanationStart a function with one pointer

    Include <pthread.h> and build with -pthread. A worker has the signature void *worker(void *arg). pthread_create(&t, NULL, worker, arg) starts it, requests default attributes, and stores its thread ID in t.

    The call returns 0 on success or an error number directly. Check that result before treating the thread as created. The new thread may run immediately; initialization must already be complete.

  8. 8
    ExampleSplit the sum into separate jobs

    Prepare two structs holding lo, hi, and result: one range is 1–50 and the other is 51–100. Pass a different struct address to each worker.

    Each worker adds its range into a private local accumulator, then writes its own result field. The first obtains 1,275 and the second 3,775. No worker writes the other’s result, so the design avoids competing updates to one accumulator.

  9. 9
    ExampleWait before reading the answers

    Create both workers before joining either, so their computations can overlap. Then main calls pthread_join for each successful creation. A join waits for its target’s completion and makes that worker’s writes visible to the joiner.

    Main can now read the two separate result fields and add 5,050. Joining just one worker would not establish that the other worker had finished its own result.

  10. 10
    TipKeep arguments and results alive

    An argument pointer does not extend an object’s lifetime. Keep creator-owned structs alive until the workers finish. A returned pointer can refer to heap memory or suitable creator-owned storage; it cannot safely point to the worker’s finished local variable.

    In a creation loop, pass distinct stable array elements instead of repeatedly passing &i. The latter points every worker at the same changing loop variable.

  11. 11
    ChallengeWhich result is ready?

    Two workers write separate result fields. Main successfully joins worker A, then reads both fields while worker B may still run. Is that whole read safe?

    ---reveal---

    A’s result is ready and visible after the join. B’s field may still be changing, so main must also join B before reading it in this design. Separate storage prevents worker-worker interference; joining establishes each worker-main handoff.

  12. 12
    TipDistinguish ending a thread from ending its process

    Returning from main or calling exit() terminates the whole process, including unfinished workers. Main normally joins required workers before returning; pthread_exit() ends only its calling thread.

    A detached worker reclaims its thread resources automatically when it finishes and cannot be joined. Detaching does not keep the process alive or give the worker a private copy of the creator’s argument objects.

  13. 13
    ChallengeEnd the program at the right time

    Main starts two workers reading its local array, detaches one, and joins the other. The detached worker is still running. Does that worker’s separate stack let it finish using the array after main returns?

    ---reveal---

    No. Returning from main ends the entire process, including that worker. Its separate stack also does not copy the array: the pointer reaches main’s object in their shared address space. Joining one worker establishes only that worker’s completion.

  14. 14
    RecapCheck the worker handoff

    For a worker design, locate its execution state, the objects it shares, and the lifetime of each argument and result. Then identify the event that makes a result ready for its reader.

    Distinct result objects plus joins can avoid locks entirely. Shared updates while workers overlap need additional coordination. Creation order, separate stacks, and detachment each answer a different question; none establishes that another thread has completed its writes.

2

Trace the operation that needs protection

11 steps
Follow a lost update, distinguish two kinds of race, and protect a decision together with its action.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionExpand the shared operation
  2. 2
    ExplanationSeparate memory from private registers
  3. 3
    ExamplePause before the store
  4. 4
    ExampleResume with the stale result
  5. 5
    ChallengeChange the scheduling point
  6. 6
    TipChoose a synchronization repair
  7. 7
    ExplanationDistinguish the two races
  8. 8
    ExampleApprove a withdrawal twice
  9. 9
    Key InsightName the complete section
  10. 10
    ChallengeTrace approval and decrement together
  11. 11
    RecapFind the boundary before the lock
3

Review a mutex protection rule

14 steps
Use one shared guard, choose its scope, and follow every success and error path through release.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionMake the agreement visible
  2. 2
    ExplanationInitialize before publishing the object
  3. 3
    ExamplePut the full update inside
  4. 4
    ChallengeRead a trylock result
  5. 5
    TipChoose the mutex type deliberately
  6. 6
    ExplanationLet independent regions overlap
  7. 7
    ExampleCompute the serialization ceiling
  8. 8
    ChallengeShorten the right work
  9. 9
    Key InsightMeasure three different outcomes
  10. 10
    ExampleFollow the early return
  11. 11
    ExplanationKeep one guard and one lifetime
  12. 12
    ChallengeCatch a callback cycle
  13. 13
    ChallengeChoose a complete review repair
  14. 14
    RecapRead code through ownership
4

Build ownership, fair turns, and efficient waiting

21 steps
Move from broken flag tests to atomic locks, then compare ticket order, sleeping protocols, and priority inheritance.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionBuild the lock in layers
  2. 2
    ExampleTwo callers pass the same test
  3. 3
    ExplanationOrdering is part of the algorithm
  4. 4
    ChallengeWould interrupt control protect two cores?
  5. 5
    ExplanationExchange the flag atomically
  6. 6
    ExampleMake a portable spin flag
  7. 7
    ExampleA failed compare is useful information
  8. 8
    ExplanationKnow the other atomic results
  9. 9
    ChallengeRetry the update, not the old answer
  10. 10
    Key InsightExclusion does not promise a turn
  11. 11
    AnalogyUse a numbered service line
  12. 12
    ExampleAdvance service one owner at a time
  13. 13
    ChallengeSeparate the dispenser from service
  14. 14
    ExplanationYielding and sleeping change different states
  15. 15
    ExampleFind the lost wakeup window
  16. 16
    ExplanationCompare and block as one decision
  17. 17
    ChallengeA wakeup arrives before the wait
  18. 18
    ConnectionSpin briefly when the owner can finish
  19. 19
    ExampleSee priority inversion through the owner
  20. 20
    ChallengeCombine scheduling with the lock
  21. 21
    RecapAsk what the waiter is waiting for
5

Trace shared counters, nodes, and buckets

16 steps
Work through batched counters, list publication, the moving queue dummy, and bucket-level locking.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionFind the shared boundary
  2. 2
    ExplanationSpread frequent counter writes
  3. 3
    ExampleTransfer one complete batch
  4. 4
    Key InsightPut a bound on the missing increments
  5. 5
    ChallengeChoose freshness for a new workload
  6. 6
    TipFreeze transfers for an exact snapshot
  7. 7
    ExamplePublish the new list node last
  8. 8
    ExplanationReaders and removers share the guard
  9. 9
    ChallengeMove through per-node locks
  10. 10
    ExampleInitialize the queue with no real item
  11. 11
    ExampleMove the dummy during dequeue
  12. 12
    Key InsightThe empty case joins the two ends
  13. 13
    ChallengeFollow the next dummy
  14. 14
    ConnectionSeparate hash buckets, preserve the mapping
  15. 15
    ChallengeCombine a bucket guard with safe insertion
  16. 16
    RecapMeasure the complete design

Threads and Locks practice questions

40 Practice Questions in 4 sets

Set 1: Thread lifetimes and shared updates10 Practice Questions

Question 1 of 10

In a load-add-store model, a counter starts at 36. A and B each load it, each adds 1 privately, and then A and B store in that order. What is the final counter?
  1. 36
  2. 38
  3. 37
  4. 39
Show answerHide answer
Answer: C
Both private calculations start from 36, so both stores write 37. The second store overwrites the same value; it does not perform another addition to shared memory. A final value of 38 would require one thread to load after the other had stored, which this schedule excludes.

Question 2 of 10

Two threads belong to the same process. Which execution state does each keep separately?
  1. Its executable code and heap
  2. Its program counter and registers
  3. Its open files and process ID
  4. Its global variables and page tables
Show answerHide answer
Answer: B
Each thread needs its own program counter and registers so it can resume its own unfinished execution. The heap belongs to the shared address space, so separate execution paths do not give the threads separate heap objects. An object becomes private through how it is used, not simply because a thread allocated it.

Question 3 of 10

A single-core server switches between two request threads while one waits for a network reply. How should their execution be described?
  1. Parallel, without concurrent progress
  2. Concurrent, with parallel execution
  3. Sequential, without overlapping progress
  4. Concurrent, without parallel execution
Show answerHide answer
Answer: D
The requests make progress during overlapping periods, which is concurrency. One core executes only one thread at an instant, so the threads are not executing in parallel. Overlapping a network wait with CPU work does not require two threads to execute simultaneously.

Question 4 of 10

A worker returns a pointer to an allocated result. Which call waits for thread t and places that pointer in void *result?
  1. pthread_join(t, &result)
  2. pthread_join(t, result)
  3. pthread_create(&t, NULL, result, NULL)
  4. pthread_detach(t)
Show answerHide answer
Answer: A
The second join argument is the address of the variable that will receive the returned pointer. Passing result itself supplies its current pointer value instead of the receiving location. Detaching permits automatic resource reclamation but removes the ability to join and collect that return value.

Question 5 of 10

An atomic balance starts at 90. Two threads each atomically read it, approve a request for 60, then each performs an atomic subtraction of 60. What failure remains possible?
  1. One approval is lost, leaving a balance of 30.
  2. The atomic accesses form a C data race.
  3. Both approve, leaving a balance of −30.
  4. Both subtractions merge into a single update.
Show answerHide answer
Answer: C
Both checks can see 90 before either subtraction, so both requests are approved and the two atomic subtractions leave −30. The atomics avoid a data race and preserve both subtractions. They do not keep the earlier balance check valid until its dependent action completes.

Question 6 of 10

A call to pthread_mutex_trylock(&m) returns EBUSY. What ownership does the calling thread have from that call?
  1. It has acquired no ownership of m.
  2. It owns m until it next blocks.
  3. It shares m with the current holder.
  4. It owns m once the holder unlocks it.
Show answerHide answer
Answer: A
The busy return means the acquisition attempt did not succeed, so the caller has no ownership and cannot enter the guarded section. Trylock does not reserve a future handoff. The thread may perform independent work or attempt acquisition again later.

Question 7 of 10

A launcher starts two workers by repeatedly passing &i, the address of its local loop index, then returns while they run. Main wants separate results from both. Which replacement design makes the argument and result handoffs safe?
  1. Main keeps one changing job struct alive, starts both workers, then joins each before reading its result.
  2. The launcher keeps two local job structs, starts both workers, returns, and main later joins before reading.
  3. Main keeps two distinct job structs alive, starts both workers, then reads results before joining either.
  4. Main keeps two distinct job structs alive, starts both workers, then joins each before reading its result.
Show answerHide answer
Answer: D
Each worker needs a distinct stable argument object, and both objects must outlive worker use. Main must then join each worker before reading the result it may still be writing. Joining cannot repair a local object whose lifetime already ended, or two workers receiving the same changing argument.

Question 8 of 10

A program loses shared increments on one core. Why can adding volatile to the counter fail to repair it?
  1. The volatile qualifier forces all threads to share registers.
  2. The load and store can still be separated by a thread switch.
  3. The counter becomes private to the currently running thread.
  4. The qualifier postpones thread switches until after a load.
Show answerHide answer
Answer: B
Volatile does not combine a read, calculation, and write into one atomic update or synchronize the threads. A context switch can still place another update between the load and store. The counter’s registers are private per thread, which is exactly why the resumed thread can retain a stale result.

Question 9 of 10

A capacity value starts at 7. Requests for 3, 5, and 2 each check it under a mutex before any decrement occurs, so all approve. They then decrement under that mutex in the same order. What final value results, and which mutex boundary repairs the rule?
  1. Final 2; retain separate ownership intervals for checking and decrementing.
  2. Final −3; retain ownership across each check and its conditional decrement.
  3. Final −3; keep the separate intervals but make each access atomic as well.
  4. Final −1; retain ownership only during each already-approved decrement.
Show answerHide answer
Answer: B
All three requests approve from 7, so their later decrements leave 4, then −1, then −3. Continuous ownership across each check and its dependent decrement prevents the stale approvals. Atomic individual accesses or already-protected subtractions do not close the interval between the decision and action.

Question 10 of 10

T1 owns a mutex while changing two related fields, then is preempted. T2 calls pthread_mutex_lock on the same mutex. What happens to the protected access?
  1. T2 waits for acquisition; T1 may resume and finish its update.
  2. T2 enters because T1 stopped executing on the processor.
  3. T2 takes ownership and completes T1’s unfinished update.
  4. T1 loses ownership and must restart both field changes.
Show answerHide answer
Answer: A
Ownership survives preemption, so T2 cannot enter merely because T1 is off the CPU. T1 can resume with the mutex still held, complete the two-field update, and release it. A mutex controls entry to protected code; it does not require its owner to run without interruption.
Set 2: Mutex boundaries and atomic ownership10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

One mutex protects every bucket in a table. What kind of lock granularity is this?
  1. Fine-grained locking
  2. Hand-over-hand locking
  3. Coarse-grained locking
  4. Per-core locking
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A list lookup locks its mutex, returns immediately on a match, and unlocks only after the loop on a miss. Why can later operations stall?
  1. A successful lookup returns while retaining the mutex.
  2. A missed lookup releases the mutex before reading the head.
  3. A successful lookup destroys the node’s mutex automatically.
  4. A returned result transfers mutex ownership to the caller.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

In a flag protocol, A observes a clear flag and pauses before setting it. B observes it clear, sets it, and enters. What can happen when A resumes?
  1. A must retest the flag before its pending assignment.
  2. A sets the flag and also enters, violating exclusion.
  3. B’s assignment changes the value A already observed.
  4. A’s context switch cancels its completed availability test.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

An atomic exchange stores 1 into a flag that currently holds 0. Which value does test-and-set return?
  1. 1
  2. The flag’s address
  3. The thread’s ticket
  4. 0
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A 16-core service uses one mutex per account. A transfer takes its source mutex first and its destination mutex second. Concurrent transfers can go in opposite directions. What rule permits independent accounts to stay concurrent while fixing the lock-order problem?
  1. Acquire the source first and release it before locking the destination.
  2. Acquire the destination first on every transfer in both directions.
  3. Acquire one global mutex around every operation on every account.
  4. Acquire both account mutexes in increasing account-ID order.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A CAS loop reads a counter at 12 and prepares 13. Another thread changes the counter to 13 before the CAS. How does the first thread preserve its increment?
  1. Repeat the same expected-12 attempt until it succeeds.
  2. Retry from the new counter value and attempt to store 14.
  3. Store its prepared 13 without checking the counter again.
  4. Treat the failed attempt as an already completed increment.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A ticket lock has turn == 6 and next unused ticket == 9. A new caller takes a ticket. Which number does it wait for?
  1. 9
  2. 6
  3. 8
  4. 10
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

Workers copy a registry struct containing a mutex and a pointer to shared records. They lock their copies, return early on validation failure, and read status using separate local mutexes. Which repair establishes one protection agreement and releases ownership on failure?
  1. Use the original registry by pointer, guard writes with its mutex, and let readers retain private local guards.
  2. Initialize each copied mutex, guard reads and writes with that copy, and route failures through its unlock.
  3. Use the original registry by pointer, guard reads and writes with its mutex, and route failures through unlock.
  4. Use the original registry by pointer, guard reads and writes with its mutex, and return directly on failure.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

Two threads run Peterson’s protocol. Their own flag stores remain buffered, so both later flag reads see the other as uninterested. Only turn is atomic; the flags are ordinary C integers. What conclusion and repair follow?
  1. Only one can enter; an atomic turn alone preserves the entire protocol.
  2. Both can enter; use properly ordered atomic accesses for flags and turn.
  3. Both can enter; marking only the ordinary flags volatile repairs the order.
  4. Only one can enter; separate cache lines eliminate ordinary flag data races.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Every update of a pair of shared fields uses mutex m, but a diagnostic function reads both fields without m. What must the diagnostic do to obtain a consistent protected pair?
  1. Declare its local copies volatile.
  2. Hold a private mutex for the reads.
  3. Hold m while reading both fields.
  4. Read the first field twice unlocked.
Show answerHide answer

Save this lesson to see this answer.

Set 3: Waiting protocols and shared structures10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A lock waiter is asleep on a kernel wait queue. What CPU time does it consume while it remains blocked?
  1. A full time slice on each scheduler cycle.
  2. One core continuously until the owner releases.
  3. A short spin on every protected memory access.
  4. None for executing that blocked thread.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A holds ticket 20; B and C hold 21 and 22. A releases, B is descheduled, and running C keeps waiting. D now takes the next ticket, with no further arrivals. What is the next-unused ticket, and what entry order must follow once B can run?
  1. Next unused 23; B, then C, then D.
  2. Next unused 24; C, then D, then B.
  3. Next unused 24; B, then C, then D.
  4. Next unused 22; B, then D, then C.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A waiter decides to park, pauses, and receives an unpark before calling park. The unpark is discarded. What is the resulting risk?
  1. It can sleep after the only wakeup has already passed.
  2. It can acquire two tickets for the same queue entry.
  3. It must spin continuously until the next time slice.
  4. It wakes immediately because park always remembers unpark.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

An approximate counter uses three local slots and threshold 4. Starting from zero, their update totals are 6, 1, and 3. After all updates finish, what are the global count and true total?
  1. Global 8; true total 10.
  2. Global 4; true total 10.
  3. Global 6; true total 10.
  4. Global 4; true total 6.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

Before linking a newly allocated list node into any shared structure, which work can be done without the list mutex?
  1. Read the shared head for a later link update.
  2. Change the shared head to publish the new node.
  3. Initialize the private node’s key field.
  4. Traverse shared nodes that removal may free.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A queue contains dummy R followed by nodes holding 14 and 29. After one successful dequeue, which node is freed and which becomes the dummy?
  1. Free the node holding 14; R remains the dummy.
  2. Free R; the node holding 14 becomes the dummy.
  3. Free R; the node holding 29 becomes the dummy.
  4. Free the node holding 29; R remains the dummy.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A fixed hash table uses 17 buckets, key % 17, and one mutex per bucket. Workers update keys 8, 25, and 26. Which pair must serialize, and which key uses a separate lock?
  1. 8 and 25 serialize; 26 uses a separate lock.
  2. 8 and 26 serialize; 25 uses a separate lock.
  3. 25 and 26 serialize; 8 uses a separate lock.
  4. All three serialize; each uses the same bucket lock.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

An approximate counter has four local slots and unit increments. Its lag after completed updates must never exceed 20. Choose the largest integer threshold meeting that limit. The slots then perform 17, 8, 5, and 0 updates from zero. What threshold and global reading result?
  1. Threshold 5; global reading 25.
  2. Threshold 7; global reading 21.
  3. Threshold 6; global reading 30.
  4. Threshold 6; global reading 18.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A hand-over-hand traversal holds the current node lock. In what order should it move to the next node?
  1. Release the current lock, then acquire the next lock.
  2. Release the current lock, then copy the next pointer.
  3. Acquire all later node locks before releasing any lock.
  4. Acquire the next lock, then release the current lock.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

On one strictly priority-scheduled CPU, low-priority L holds a lock, high-priority H spins for it, and medium-priority M is ready. First the lock is changed to a ticket spin lock. Then it is replaced by a priority-inheriting mutex on which H blocks. What progress does the second change permit that ticket order alone does not?
  1. L runs ahead of M, releases, and lets H proceed; ticket order alone leaves H preventing L from running.
  2. H enters ahead of L, releases, and lets M proceed; ticket order alone requires M to release first.
  3. M runs ahead of L, releases H’s mutex, and lets H proceed; ticket order alone blocks all priorities equally.
  4. L stays below M until M finishes; ticket order alone would instead boost L to the priority of H.
Show answerHide answer

Save this lesson to see this answer.

Set 4: Review concurrent designs across boundaries10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A store-conditional fails after a load-linked even though the code has no evidence of another writer. How should the update routine respond?
  1. Treat the failed store-conditional as successful.
  2. Write the value with an ordinary unconditional store.
  3. Retry the LL/SC sequence with a fresh load-linked.
  4. Return the old value and report the update committed.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A joinable worker will never have its return value collected and must reclaim its thread resources when it ends. What pthread operation arranges this?
  1. Call pthread_join without waiting for it.
  2. Detach the worker with pthread_detach.
  3. Return from main before the worker ends.
  4. Copy the worker’s pthread_t locally.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

W observes a futex lock word at 1. The owner clears it to 0 and wakes before W calls wait. C then acquires it, setting it to 1. W calls futex wait expecting 1, and C later clears it and wakes. Can W block at that call, and does the later wake grant ownership?
  1. W can block on C’s held state; after waking it must retry acquisition.
  2. W cannot block because an earlier owner already sent a wake notification.
  3. W can block on C’s held state; the later wake automatically grants ownership.
  4. W must block forever because its earlier read referred to a different owner.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A two-lock queue is empty at dummy D. E prepares node R with value 11, publishes D->next = R, and pauses before moving tail. Q then successfully dequeues; E afterward finishes moving tail to R. What state remains, and how must the cross-end next accesses be implemented in C11?
  1. One item 11 at R; publish and read next atomically with suitable ordering.
  2. Empty at dummy R; ordinary next accesses are ordered by the separate end mutexes.
  3. Empty at dummy D; volatile next accesses provide the required publication order.
  4. Empty at dummy R; publish and read next atomically with suitable ordering.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

Every request holds one shared mutex for 5 µs. Ignoring all other costs, what upper throughput limit does that protected work impose?
  1. 5,000 requests per second.
  2. 1,000,000 requests per second.
  3. 200,000 requests per second.
  4. 5,000,000 requests per second.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A counter has one local slot at 5 and global at 40. A reader copies global under its guard and releases it. A worker transfers the local 5 globally and resets it. The reader then copies local under its guard. What total does the reader compute, and how can it obtain an exact snapshot?
  1. It computes 40; hold all local locks then the global lock for the snapshot.
  2. It computes 45; read global twice without taking any local locks.
  3. It computes 50; lock only the global counter during the entire snapshot.
  4. It computes 40; lock only each field briefly while copying that field.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A cache update holds a normal mutex and invokes a callback that queries the same cache. Moving the callback after unlock would avoid reentry, but its raw node pointer could then be freed by another worker. Which redesign permits the callback outside the lock while keeping its input valid?
  1. Keep the raw node pointer, unlock before the callback, and let the callback acquire the same mutex when it starts.
  2. Copy the needed fields into callback-owned storage under the mutex, unlock, then call the callback on that copy.
  3. Copy the fields into callback-owned storage, keep the mutex held, and let the callback query through the same mutex.
  4. Keep the raw node pointer, unlock, and mark the pointer volatile so a later removal cannot invalidate the node.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

Two lookups find a node. A copies its integer value into caller-owned storage under the list mutex. B saves a pointer and unlocks. A remover then unlinks and frees the node; its address may be reused before B reads. Which result remains valid, and what must B’s interface provide?
  1. Both results remain valid; B only needs to lock the list again before reading the saved address.
  2. Only B’s pointer remains valid; A must lock the list whenever it reads the copied value.
  3. A’s copied value remains valid; marking B’s saved pointer volatile preserves the removed node.
  4. A’s copied value remains valid; B needs a value copy or protection of the node’s continued lifetime.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A ticket lock’s next unused ticket is 15 and its current turn is 12. Three callers take tickets before any release. What are their tickets and the new dispenser value?
  1. Tickets 15, 16, 17; next unused 18.
  2. Tickets 12, 13, 14; next unused 15.
  3. Tickets 16, 17, 18; next unused 18.
  4. Tickets 15, 15, 15; next unused 18.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A table grows from 11 to 17 buckets using key modulo bucket count. A worker has selected the old bucket for key 24. The resizer locks only its new destination bucket, moves the entry, and frees the old bucket array. Which destination and transition rule preserve this simple design?
  1. Bucket 2; keep the old mapping while publishing the new bucket count to ordinary operations.
  2. Bucket 7; protect only the new destination because the old worker computed its index earlier.
  3. Bucket 7; exclude ordinary table operations and finish old users before retiring old storage.
  4. Bucket 2; protect only the old source while freeing every old bucket after the entry moves.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

75 Key Terms, each with its definition

A

Address space
An address space is the set of memory addresses available to a process; its threads share the code and objects within it.
Two worker stacks occupy different regions of one address space, so a pointer can cross between them.
Approximate counter
An approximate counter batches guarded local updates into a guarded global count whose reading can lag the exact total.
A statistics collector can tolerate recent increments staying local until their transfer threshold is reached.
Atomicity
Atomicity means an operation appears indivisible to relevant observers, so they cannot observe its intermediate partial state.
Using one shared mutex can make a two-field update atomic for other users of that same mutex.

B

Blocked thread
A blocked thread cannot be selected to execute until an event makes it runnable again, so it uses no CPU while blocked.
A sleeping mutex waiter leaves the runnable set until a wakeup allows it to compete again.
Bounded waiting
Bounded waiting limits how many competing critical-section entries may precede a thread after it has requested entry.
Earlier outstanding tickets bound the number of entries before a ticket-lock waiter’s turn.
Bucket lock
A bucket lock protects one hash bucket’s entries, allowing operations on different buckets to proceed under different guards.
Two keys with the same bucket index contend for the same guard despite having different values.

C

Cleanup path
A cleanup path is a common exit sequence that releases acquired resources before a function returns from success or failure.
A lookup stores its return value, breaks from the loop, and reaches one shared mutex unlock.
Coarse-grained locking
Coarse-grained locking protects a large structure or subsystem with one guard, simplifying correctness but limiting independent access.
One list mutex makes every lookup wait behind an insertion even when its target lies far away.
Compare-and-swap
Compare-and-swap atomically replaces a value only when it matches an expected value, reporting enough information to detect failure.
A counter updater retries when a competing increment makes its expected value stale.
Concurrency
Concurrency is progress by several executions during overlapping periods, including interleaved execution on a single CPU core.
A timer can switch between concurrent workers even when the machine can run only one at an instant.
Contention
Contention occurs when threads compete for a shared resource, such as attempting to acquire a mutex that another thread holds.
Adding workers to one locked counter can increase waiting and shared-cache traffic instead of throughput.
Counter wrap-around
Counter wrap-around occurs when unsigned arithmetic passes the maximum representable value and continues from zero.
Ticket equality stays usable across wrap-around only while outstanding ticket numbers remain unique.
Critical section
A critical section is a shared-state code region that must exclude competing operations to preserve the operation’s correctness.
A withdrawal’s critical section begins before the balance check and includes the conditional subtraction.

D

Data race
A data race is conflicting unsynchronized access to one memory location by different threads, including a write and a non-atomic access.
A status read can race with an ordinary counter write even though the status function changes nothing.
Detached thread
A detached thread reclaims its thread resources automatically when it terminates and cannot be joined to collect its return value.
A background worker may be detached, but returning from main still ends the process containing it.
Dummy node
A dummy node anchors a linked queue without representing a current item; its next node, when present, holds the first queued value.
A dequeue returns the next node’s value, frees the old dummy, and makes the value node the new dummy.

E

Error-checking mutex
An error-checking mutex reports specified ownership mistakes, including owner relocking and release by a thread that does not own it.
During development, a relock attempt can return EDEADLK instead of silently blocking the same thread.
Exact snapshot
An exact snapshot reads related shared values consistently at one protected state, preventing transfers from splitting the observation.
An approximate counter snapshot holds the local guards and global guard while adding all counts.

F

Fetch-and-add
Fetch-and-add atomically adds an amount to a stored value and returns the value that existed before the addition.
A ticket dispenser returns 12 to one caller while advancing its next available number to 13.
Fine-grained locking
Fine-grained locking gives separate guards to smaller regions of shared state, allowing independent operations to proceed concurrently.
Separate account mutexes let two unrelated account pairs be updated at the same time.
First-come, first-served order
First-come, first-served order serves requests in their recorded arrival order, preventing later recorded arrivals from overtaking earlier ones.
For a ticket lock, arrival is recorded when the caller atomically obtains its ticket number.
Futex
A futex is a user-memory word with Linux kernel support for atomically checking an expected value and blocking, plus waking waiters.
An uncontended mutex can use user-space atomics and call the kernel only for contended waiting.

H

Hand-over-hand locking
Hand-over-hand locking acquires the next node’s guard before releasing the current one, preserving protection during traversal.
Different traversals can occupy separate list regions, though every visited node adds locking overhead.
Hash bucket
A hash bucket is a table region selected by a key’s hash mapping, holding entries that share that mapped location.
Modulo 101 places keys 7 and 108 in the same bucket even though their numeric values differ.
Hash collision
A hash collision occurs when distinct keys map to the same table bucket, making them share a lookup region and its guard.
With 13 buckets, keys 2 and 15 collide because each has remainder 2 when divided by 13.

I

Interleaving
An interleaving is an order in which steps from different threads occur while preserving the relevant within-thread execution order.
Placing one thread’s complete update between another thread’s load and store reveals how a stale result can overwrite it.

J

Join
A join waits for a joinable thread to terminate and makes its completed writes visible to the thread that successfully joins it.
Main joins two sum workers before reading and combining their separate result fields.

L

Load-linked/store-conditional
Load-linked/store-conditional pairs a reservation-setting read with a write attempt that can fail and must be retried when unsuccessful.
An invalidated reservation makes store-conditional fail instead of overwriting a competing change.
Lock granularity
Lock granularity is the amount of shared state one lock protects, which determines which operations must serialize together.
Moving from a table mutex to per-bucket mutexes lets operations in different buckets overlap.
Lock ownership
Lock ownership is the exclusive right obtained through successful acquisition, retained until the owning thread releases the mutex.
A context switch pauses the owner without granting a waiting thread permission to enter.
Lock-free atomic operation
A lock-free atomic operation supplies its atomic effect without depending on another thread to release an internal lock.
Other atomic objects may use a library lock, so atomic correctness alone does not establish lock-freedom.
Lost update
A lost update occurs when a thread stores a result based on an old read and overwrites the effect of a competing update.
Two workers can each load 50 from a shared counter and store 51, leaving only one visible increment after two attempts.
Lost wakeup
A lost wakeup occurs when a needed notification fails to prevent or end a wait, allowing a thread to remain blocked incorrectly.
A naive waiter can park after an earlier unpark was discarded, with no release left to wake it.

M

Many-to-one threading
Many-to-one threading maps multiple user threads to one kernel thread, preventing their simultaneous execution on different cores.
A blocking system call can stall every user thread mapped onto that one kernel execution path.
Memory leak
A memory leak is allocated storage that is no longer needed but cannot be reclaimed because the program has lost its usable reference.
Two racing front insertions can overwrite head and leave the first new node unreachable.
Memory ordering
Memory ordering specifies constraints on how memory operations become visible, including the operations surrounding lock handoffs.
Lock release must keep the protected writes before publication of the newly available state.
Memory reclamation
Memory reclamation releases allocated storage only after no operation can still access the object occupying that storage.
Unlinking a node is insufficient when a caller retained its pointer and may dereference it later.
Mutex
A mutex is a mutual-exclusion lock owned by one thread at a time and used to guard a shared operation or data region.
All balance operations acquire the same account mutex before using the protected balance field.
Mutex identity
A mutex identity is tied to the original mutex object at its initialized address; a copied pthread mutex is not a valid alias.
Pass a shared registry by pointer instead of copying its struct and using the copied mutex.
Mutual exclusion
Mutual exclusion is the property that at most one competing thread occupies a protected critical section at a time.
A second thread cannot pass the shared mutex acquisition while a first thread owns it, even if that owner is temporarily preempted.

N

Node publication
Node publication makes a prepared private node reachable through shared links so other operations can safely find its initialized data.
Front insertion fills the key first and publishes the head only while holding the shared list mutex.

O

Object lifetime
An object lifetime is the period during which a stored object exists and may be accessed through an otherwise valid reference.
Returning a pointer to a worker’s local variable fails because that object ends with the function.
One-to-one threading
One-to-one threading maps each application thread to a kernel-scheduled thread, allowing parallel execution and per-thread blocking.
Two ordinary Linux threads can be placed on separate cores by the operating system’s scheduler.

P

Parallelism
Parallelism is simultaneous execution on different processing resources, such as several threads running on distinct CPU cores.
Independent portions of an image can be processed in parallel when several cores are available.
Per-core counter
A per-core counter records updates in a slot associated with one CPU core, reducing writes to one globally shared location.
Threads using different local slots can update under different mutexes until a batch transfer is needed.
Peterson’s algorithm
Peterson’s algorithm is a two-thread exclusion protocol using interest flags and a turn variable, correct under sequential consistency.
When both threads request entry, the turn variable resolves which interested contender waits.
POSIX threads
POSIX threads are the standardized threading interface used by C programs on Unix-like systems to create and coordinate threads.
A C worker program includes <pthread.h> and is compiled and linked with the -pthread option.
Priority inheritance
Priority inheritance temporarily raises a lock owner to a waiting thread’s higher priority so the owner can run and release sooner.
Boosting the owner prevents intermediate-priority work from indefinitely delaying the required release.
Priority inversion
Priority inversion occurs when higher-priority work waits on a lower-priority owner whose progress is delayed by other scheduling activity.
Medium-priority work can keep a low-priority mutex owner from releasing for a high-priority waiter.
Progress
Progress requires contenders for a free critical section to select an entrant without indefinite obstruction by noncontenders.
A thread that is not trying to enter must not keep an otherwise available section closed forever.
Protection rule
A protection rule assigns shared state to a particular guard and specifies which accesses must acquire that same guard.
A comment next to a counter can name the mutex every concurrent read and update must acquire.

R

Race condition
A race condition occurs when whether an operation is correct depends on the relative timing of concurrent actions.
Two separately protected approvals can both use the old balance before either withdrawal is applied.
Read-modify-write
A read-modify-write operation reads a stored value, computes a changed value, and writes the result back to that shared location.
An ordinary counter increment exposes a gap between its initial load and its final store.
Recursive mutex
A recursive mutex permits repeated acquisition by its owner and becomes available only after the matching number of releases.
Two successful acquisitions by one thread require two unlocks before another thread can acquire it.
Reentrant call
A reentrant call enters an operation again before its earlier invocation finishes, potentially attempting to reacquire a held guard.
A logging callback may query the same cache while its outer update still owns the cache mutex.

S

Sequential consistency
Sequential consistency gives operations one common order that preserves each thread’s program order and is observed by all threads.
Peterson’s proof assumes the flags and turn follow this ordering instead of reordered visibility.
Spin lock
A spin lock is a mutual-exclusion lock whose unsuccessful callers repeatedly check or attempt acquisition while consuming CPU time.
A short hold on another running core can make spinning cheaper than blocking and waking.
Spin-waiting
Spin-waiting repeatedly tests a condition while remaining runnable and consuming CPU time until that condition permits progress.
A waiter can spend its entire time slice checking a flag while the preempted owner cannot run.
Starvation
Starvation is indefinite delay of a thread while competing threads keep making progress and repeatedly obtain the resource it needs.
A test-and-set waiter can lose every race after unlock because the lock remembers no arrival order.

T

Table resizing
Table resizing changes a hash table’s bucket arrangement and remaps entries, requiring coordination beyond one bucket’s ordinary guard.
A simple bucket-locked hash table excludes concurrent table operations while moving entries to new buckets.
Test-and-set
Test-and-set atomically stores a new value and returns the old value, allowing one caller to claim a free flag without a gap.
An exchange that receives 0 owns the flag; contenders receiving 1 keep waiting for release.
Thread
A thread is a sequence of execution within a process, with its own program counter, registers, stack, and thread identity.
A server can use one thread to wait for a reply while another thread processes a completed request.
Thread argument
A thread argument is data supplied to a new worker through its entry function’s pointer parameter, often pointing to a struct.
An array gives each worker a distinct argument object that remains valid until the joins finish.
Thread control block
A thread control block is an operating-system record that holds a thread’s saved execution state for scheduling and resumption.
On a switch between two threads, the first thread’s register state is saved in its thread control block before the second’s state is restored.
Thread creation
Thread creation starts a new execution path at a supplied function, with an argument pointer and configurable thread attributes.
Main prepares a range struct before passing its address to a successfully created sum worker.
Thread safety
Thread safety means a structure behaves according to its specification when operations from several threads interleave.
A counter needs a protection rule for reads as well as increments when those operations can overlap.
ThreadSanitizer
ThreadSanitizer is a runtime analysis tool that detects data races in instrumented executions and reports the conflicting accesses.
A test build using -fsanitize=thread can expose a read path that omitted the updater’s mutex.
Ticket lock
A ticket lock gives each requester a unique atomic ticket and admits it when the serving counter reaches that ticket.
A caller holding ticket 6 waits while turn is 5 even if a later-ticket caller is already running.
Transfer threshold
A transfer threshold is the local count at which an approximate counter moves its accumulated increments into the global count.
Raising the threshold reduces global acquisitions but permits more increments to remain unreported.
Trylock
A trylock is an immediate acquisition attempt that reports whether the caller obtained the mutex instead of waiting while it is held.
An EBUSY result lets a worker choose independent work, but does not grant access to guarded fields.
Turn counter
A turn counter records the ticket currently allowed to enter a ticket lock and advances once when its owner releases.
Taking a new ticket advances the dispenser while leaving the current turn unchanged.
Two-lock queue
A two-lock queue serializes enqueues with a tail guard and dequeues with a head guard, permitting opposite-end operations to overlap.
In a C11 two-lock queue, the dummy node’s next link in an empty queue still needs atomic publication and reading.
Two-phase lock
A two-phase lock waits by spinning briefly at first, then blocking if the owner has not released during that short interval.
The short spin can avoid wakeup overhead for quick holds without burning a core through long ones.

V

Volatile
The C qualifier volatile marks accesses with special observable-access rules; it supplies neither atomicity nor thread synchronization.
Marking a shared counter volatile does not combine its load, addition, and store into one update.

Y

Yield
A yield gives up the caller’s current CPU turn while leaving it ready to run again when the scheduler selects it.
Many yielding lock waiters can cause repeated context switches before the owner gets another turn.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started