---
title: 'POSIX Threads'
source: 'https://academia.sh/en/courses/c-systems-programming/posix-threads'
course: 'Systems Programming with C'
language: en
updated: '2026-09-26T03:02:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'shows that two threads incrementing the same counter under a lock produce a complete, deterministic result'
    - 'distinguishes what the lock guarantees as visibility, not ordering'
    - 'explains that threads share the memory space, unlike fork, comparing it against the classes from previous lessons'
---

# POSIX Threads

Two threads in the same process changing the same memory under a lock is not a handoff, consumed once like the bytes a pipe carries, but a continuously shared exchange.

The last lesson on system interfaces stays within the same process, but gives up the
form of carrying that crosses a process boundary and moves to two streams that share
the same memory space: the **thread**. `fork` gave a copy, a signal left a handoff,
a pipe and `mmap` crossed the inter-process boundary in two different ways. Two
threads, on the other hand, never cross a boundary at all — they share the same
process's same memory from the start. A change made under a lock therefore falls
into the course's fourth class: **shared**.

`pthread_create` also differs from `fork` in how it sets this up. The moment `fork`
is called, the kernel copies the memory's page table — thanks to copy-on-write the
physical pages are not duplicated right away, but the two processes now have separate
address spaces, and when one writes the other does not see it. `pthread_create`,
however, does not open a new address space; it only allocates a new stack and starts
a second point of execution inside the same process. The code segment, the data
segment, the heap — all a single copy, both streams see the same addresses. That is
why a global variable like `counter` is the same memory location for both threads;
compared with the two independent copies of a same-named variable after `fork`, this
difference is what directly determines this lesson's class.

## The Counter Written Under a Lock

`observe.h` and `observe.c` do not change. This lesson's own file,
`thread_measurement.c`, runs a single measurement in which two threads increment the
same counter under a lock and the result reaches exactly the expected value.

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

```c
// thread_measurement.c — two threads incrementing the same counter under a lock
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include "observe.h"

#define STEPS 1000

static long counter = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

static void *increment(void *data) {
    (void)data;
    for (int i = 0; i < STEPS; i++) {
        pthread_mutex_lock(&lock);
        counter = counter + 1;
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    observe("total written by both threads under the lock", SHARED,
           counter == 2L * STEPS);

    pthread_mutex_destroy(&lock);
    observe_summary();
    return 0;
}
```

```text
total written by both threads under the lock saw      shared
classes: copy 0, shared 1, handoff 0, unobserved 0
reached 1, not reached 0
```

**SI16.** The single line says `saw`, and this result is not a matter of chance, it
is guaranteed. Both threads run the `increment` function, each incrementing
`counter` a thousand times; the main thread looks at `counter` after both
`pthread_join` calls return and finds exactly `2000`. This is fundamentally
different from the independent copies `fork` gives: `pthread_create` does not open a
new memory copy, it starts a new flow of execution in the same address space of the
same process. The `counter` variable sits in a single place, and both streams write
to that one place — that is why the class is **shared**, not the **copy** of `fork`.
Because the `STEPS` constant was chosen to be a thousand, the total is also exact:
the two threads together perform `2 * 1000 = 2000` increments, and if the lock never
loses one, the result can only be that number — a measurement that occasionally comes
out `1998` or `2001` is not a result this lesson accepts, because the course's
measurement method only verifies deterministic results that come out the same every
time.

The line `counter = counter + 1` may look like a single C expression, but it
corresponds to three separate steps: go to memory, read the value, add one, write
the result back to memory. Without a lock these three steps are open to
interruption — while one thread is between reading and writing, the other thread
can read the same old value, both write back their own computed `+1`, and one of the
two increments is lost. `pthread_mutex_lock` turns these three steps into an
indivisible unit: the thread holding the lock finishes all three before the other
thread can acquire the lock, so no increment is ever lost. The size of `STEPS` is
also deliberate — with a small enough loop count even a lockless version might give
the correct result by chance, because the two threads' steps could proceed in
sequence without colliding; a thousand-step loop reduces that chance to practically
zero, though the course text describes this without ever running it.

Java's standard collections show a different face of the same problem: if a
collection is modified by another thread while iterating over an `ArrayList` or a
`HashMap`, most implementations throw a `ConcurrentModificationException` instead of
silently corrupting the data — this "fail-fast" behavior is the scope of M08/K03's
"Iterators and Concurrent Modification" topic. The `counter` in
`thread_measurement.c`, however, is a single `long`, not a structure being iterated
over; the lock keeps it intact, but C itself has no runtime check comparable to
Java's — code that forgets the lock and writes directly to `counter` compiles without
error and silently produces the wrong result, without giving a warning like
`ConcurrentModificationException`. The check here is not in the compiler or at
runtime, it is entirely in the programmer's discipline.

## What the Lock Guarantees: Visibility, Not Ordering

**SI17.** The `pthread_mutex_lock`/`pthread_mutex_unlock` pair wraps the line
`counter = counter + 1`, and this wrapping guarantees two separate things at once.
The first is **mutual exclusion**: two threads can never hold the lock at the same
time, so the step of reading `counter`, adding one, and writing it back is never
interrupted midway — one thread cannot start until the other has completed all three
steps. The second is **visibility**: when a thread releases the lock, the change it
made is guaranteed to be visible to the next thread that acquires the lock. Without a
lock both guarantees are lost — two threads could read the same `counter` at the same
time and add one to the same old value, and an increment could be lost.
`thread_measurement.c` never runs this lost-update case; a lockless version is
described but not executed, because its result would not be deterministic, and this
course's measurement method only verifies deterministic results.

While the lock guarantees visibility, it does not guarantee **which thread runs
first**. No matter in what order `t1` and `t2` happen to be scheduled, which one
acquires the lock more times first is never measured or printed inside
`thread_measurement.c` — only the final total is measured. This also fits the rule
the course set from the start: a thread's identity is never printed, and neither
stream prints anything among themselves, only the main thread prints a single line
after both `join` calls return.

In the Java Memory Model (JMM) a similar visibility guarantee is built with the
`volatile` keyword and synchronized blocks — M08/K04's "JVM, Concurrency, and
Performance" topic defines this through the "happens-before" relation: all writes a
thread makes before releasing a lock become visible to the next thread that acquires
that lock. The rule matches, word for word, the guarantee the
`pthread_mutex_unlock`/`pthread_mutex_lock` pair gives; the difference is that the
JMM defines this **in the language's own specification**, through the virtual
machine's memory ordering, whereas in C this guarantee comes from the POSIX thread
specification's calling contract — the `pthread_mutex_lock`/`unlock` calls
themselves constrain compiler and hardware ordering, the language has no separate
concept of a memory model of its own.

Go's concurrency model solves the same problem from the other direction. The
principle in M09/K03's "Goroutines and Channels" topic — "share memory by
communicating, don't communicate by sharing memory" — defines passing a value
through a channel as transferring that value's ownership from the sender to the
receiver; two goroutines never write to the same variable at the same time, a value
flows once over a channel. The `counter` in `thread_measurement.c` is the exact
opposite: ownership is never transferred, both threads have the right to access the
same location at the same time, and that right is bounded with a `pthread_mutex_t`.
Had the channel pattern been chosen in Go, instead of sharing `counter` each
goroutine would compute its own partial sum and send it over a channel, and the
summing step would happen at a single receiver — this would not be the **shared**
class this course measures, it would be a **handoff** pattern similar to what the
pipe carries.

## The Bounding Measurement

**SI18.** This measurement verifies only the **final value**, not the intermediate
state. Which intermediate values `counter` passes through while under the lock —
whether `1`, `2`, `3` in order, or however the two threads' steps happen to
interleave — is never measured anywhere, because measuring it would itself require an
extra locked read, and that would change the system being measured. The only thing
`thread_measurement.c` verifies is that none of the `2000` steps was lost; the
question of which step came from which thread is outside this lesson's scope. This
bound is deliberate: the course measures whether two streams see each other, it does
not reconstruct the race between them. A measurement that wanted to observe a race
condition itself — for example counting which thread acquired the lock in which
position — would become dependent on timing and would break this course's
deterministic-result rule; that is why no such measurement appears here.

This lesson's biggest limitation is that it **assumes the lock is used correctly**.
The `lock` variable is acquired before every access inside `increment`, but nothing
enforces that — if a third function were added that read `counter` without the lock,
the compiler would give no warning, the code would silently become wrong. Rust's
concurrency model moves this gap into the type system: M10/K04's "`Send` and `Sync`"
topic marks, at compile time, whether a type can safely be moved between threads
(`Send`) and whether it can safely be used as a shared reference (`Sync`). A shared
counter in Rust is not held as a bare variable but inside a wrapper like
`Mutex<i64>`, and code that tries to access that wrapper without acquiring the lock
**does not compile** — it moves an error like the one in `thread_measurement.c` from
runtime to compile time. C has no such check: even though `pthread_mutex_t lock`
sits in the same file as `counter`, the bond between them is only a rule described in
this lesson's text, not a constraint the compiler knows about. This course's
measurement method does not close that gap either — it only verifies a single run in
which the lock is used correctly, it never runs and shows a version where the lock is
forgotten.

## Next Step and a Combined View of the Classes

**SI19.** Across these five lessons the same four classes appeared with different
carriers. A variable after `fork` was a **copy** — the two sides independent, no
change ever reached the other side. The flag left by a signal handler was a
**handoff** — it reached the main flow once. The byte a pipe carried was also a
**handoff**, the second read ended in end-of-file. A mapping set up with `MAP_SHARED`
and this lesson's locked counter were **shared** — a record continuously shared,
when one side changes it the other side sees it too. The fourth class,
**unobserved**, stayed within the first lesson's own scope: cases where the
observation itself goes unseen.

It was also possible to produce the same result with a different mechanism — for
example incrementing `counter` with an atomic operation instead of a lock, or using
some other synchronization tool over shared memory instead of a thread. This lesson's
limit is that it only measures the most common pattern built with
`pthread_mutex_t`; atomic operations and other lock kinds fall outside this course's
scope.

These four classes answer a question independent of the carrier itself: can a side
other than the one that made a change see that change, and if so, when, how many
times? `fork` answers no — a copy does not show it. A signal and a pipe answer yes,
but only once — a handoff is consumed. `MAP_SHARED` and the locked counter answer yes
and continuously — a shared value is not consumed. The first lesson's observation
mechanism itself is a fourth, rotten answer to this question: one side sees
something but the measurement never catches it — unobserved. The `observe()` function
used across all five lessons tagged these four answers with a single word each and
checked on its own whether that tag was consistent with the measured `saw`/`missed`
value; no lesson skipped this check, and no measurement ended with a `MISMATCH` tag.

Because this classification looks at the **visibility pattern**, not the carrier, it
can also be applied to concurrency tools in other languages — the JMM in M08/K04,
the channel in M09/K03, `Send`/`Sync` in M10/K04 all answer the same four questions
in their own syntax, only where the guarantee is built (runtime, type system,
language specification) changes. C's POSIX thread and process interface always
builds this guarantee into the calling contract: the `fork` call itself defines the
copy class, the `pthread_mutex_lock`/`unlock` pair the shared class, signal delivery
and pipe reads the handoff class. System interfaces close here: five lessons
answered, with four classes, the question of who outside a process or a thread can
see a change.

## Summary

- Two threads share the same memory space within the same process; unlike `fork`, no
  separate copy is opened.
- This lesson's class is **shared**: a change made under a lock is visible to both
  sides, continuously shared like a record.
- `pthread_mutex_lock`/`unlock` give two guarantees: mutual exclusion and
  visibility. They do not guarantee which thread runs first.
- The measurement verifies only the final total; it does not measure the
  intermediate state or the ordering between threads, because measuring that would
  itself become dependent on timing.
- A lockless version is only described in prose, never run; because its result is
  not deterministic, it does not fit this course's measurement method.
- The same visibility question is solved in different places in other languages: in
  the JMM's `happens-before` relation in Java, in channel ownership transfer in Go,
  in the `Send`/`Sync` types in Rust — in C only in the `pthread_mutex_t` calling
  contract, with no compiler support.

## Next Step

System interfaces are complete here. Across five lessons the same `observe()` unit
measured five different carriers — copied memory, the flag left by a handler, a
pipe, a shared page, a locked counter — with the same four classes; none replaced
another, because each answered a different form of the same question, "who sees what
and when." The next lesson (Linked Structures) carries the same observation unit out
of the process and thread boundary and into a single data structure: when two
pointers point at the same node, does one see the change the other makes?
