C++ / a working model

131 / 163   ·   C11   ·   8 min

Interlude: Thread API

Keep this sentence

This interlude surveys the core POSIX thread-library calls used to launch new flows of execution, wait for them to finish, and protect shared data with mutexes. The interfaces balance ease of use with flexibility; later chapters expand on locks and condition variables through many examples.

In this lesson
  1. Creating Threads
  2. Waiting for Thread Completion
  3. Mutex Locks
  4. Safe Argument and Return-Value Passing
  5. Example
  6. Exercise

Official chapter PDF

Creating Threads

To obtain an extra concurrent flow of execution a program calls pthread_create. The caller supplies storage for a pthread_t identifier, an optional attributes pointer (NULL selects default stack size and scheduling), a function pointer naming the thread’s entry point, and a single void* argument. The start routine must have the signature void*(*)(void*), so any data can be supplied by casting or by packing fields into a structure. After a successful call the new thread possesses its own stack yet shares the same address space as every other thread in the process.

Waiting for Thread Completion

Creation alone is rarely sufficient; the caller usually needs to know when work has finished and to collect results. pthread_join blocks the calling thread until the identified pthread_t terminates and optionally writes the thread’s return value through a void**. Passing NULL for the second argument is legal when the value is unused. Long-running servers may never join, yet compute-oriented parallel programs almost always join so that every piece of work completes before the next stage or process exit.

Mutex Locks

Simultaneous reads and writes of the same variable by several threads produce data races. POSIX supplies pthread_mutex_t for mutual exclusion. A lock may be initialized statically with PTHREAD_MUTEX_INITIALIZER or dynamically with pthread_mutex_init. A thread calls pthread_mutex_lock before entering a critical section and pthread_mutex_unlock after leaving it. Only one thread holds the lock at any instant, serializing modifications to the shared state.

Safe Argument and Return-Value Passing

The void* type lets any data travel in or out. A single integer can be cast directly; several values are packed into a programmer-defined struct whose address is passed. When a thread must hand results back to the joiner the struct must be allocated with malloc on the heap. Returning the address of a stack-allocated local is forbidden: the stack frame vanishes as soon as the function returns, leaving a dangling pointer.

Pitfalls

  • Returning the address of a stack-allocated local from a thread function yields a dangling pointer and undefined behavior.
  • Creating threads without a matching join can let the main thread exit too early, forcibly killing workers.
  • Touching shared variables without holding the protecting mutex creates data races.
  • Calling lock or unlock on an uninitialized or already-destroyed mutex produces crashes or deadlocks.

Run an example

Minimum C11 · complete program · Download .c

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

#define N 1000

int counter = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *worker(void *arg) { (void)arg;
    int i;
    for (i = 0; i < N; i++) {
        pthread_mutex_lock(&lock);
        counter++;
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Final counter: %d\n", counter);
    return 0;
}

Compile locally

gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-27-thread-api.c -o example && ./example

Expected result

Final counter: 2000

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

A thread function allocates a struct on its own stack and returns its address. What happens when the main thread dereferences that pointer after pthread_join, and why?

Show a reference answer

The dereference reads deallocated memory, producing garbage or a crash. The thread’s stack frame is reclaimed on return, instantly invalidating the pointer.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog