ECE344 Fall 2026 (Sec 1) Lec 13 - Threads
Watch on YouTube →
Overview
Jon Eyolfson explains how threads differ from processes and how concurrency differs from parallelism, then demonstrates thread creation, scheduling, termination, and cleanup with POSIX Pthreads. The lecture emphasizes the trade-off between threads’ shared memory and low context-switch cost and processes’ isolation, and uses runnable C examples to show why joining threads and passing arguments with safe lifetimes matter.
Key takeaways
- Concurrency means switching among tasks so each can make progress; parallelism means executing tasks at the same instant, and neither property guarantees the other.
- Threads in one process have separate registers and stacks but share the page table, code, globals, heap, and file descriptors, making communication cheap but synchronization and memory ownership important.
- A same-process thread context switch avoids changing the page table and the associated TLB flush, while a process switch can provide stronger isolation at greater cost.
- `pthread_create` does not determine execution order, and returning from `main` exits the process; use `pthread_join` or let the main thread call `pthread_exit` when workers must finish.
- Joinable threads retain resources after termination until joined; detached threads clean up automatically but cannot be joined or queried for their return values.
- A worker must not rely on a pointer to a creator’s stack variable remaining valid; allocate persistent argument data on the heap, transfer it safely, and release it when finished.
Chapters
0:00
From Processes to the Threads Unit
- The lecture introduces threads as the next major operating-systems topic after the first test.
- The central distinction is between concurrency—making progress on multiple tasks by switching—and parallelism—executing tasks at the same instant.
6:50
Concurrency vs. Parallelism Through Dinner Tasks
- Talking and gesturing can happen in parallel because they use different capabilities, while drinking and talking cannot happen simultaneously under the analogy’s civilized-eating rule.
- Drinking and talking can still happen concurrently by switching between sips and speech; eating and drinking cannot even be concurrent under the rule that eating cannot be interrupted.
- Concurrency and parallelism are independent: one does not imply the other.
11:26
A Thread Is a Process’s Execution Context
- The kernel schedules a thread, defined here by its own registers and stack; the saved register state is stored in a trap frame.
- A process begins with one main thread, and additional threads receive independent registers and stacks while using the process’s page table.
- Threads in one process share code, globals, heap, and file descriptors; a heap allocation can be shared, while handing another thread a stack address is generally unsafe.
13:15
Thread-Local State, Concurrency, and Multicore Execution
- A global-variable update is visible to every thread in the process; thread-local storage provides per-thread state instead.
- The C library’s `errno` is thread-local, preventing one thread’s error value from overwriting another’s.
- One CPU core can time-slice several threads for concurrency, while multiple cores can execute threads in parallel.
- A server can use a thread per request so a blocked request does not prevent other requests from progressing, even without parallel execution.
19:30
Choosing Between Processes and Threads
- Separate processes have separate virtual memory and require IPC such as pipes, signals, or explicit shared memory; threads communicate through their shared address space.
- `fork` creates a process with a new page table, while `pthread_create` is cheaper because it adds a thread stack without duplicating the process address space.
- Switching between threads in one process avoids changing the page table and the associated TLB flush; process switches may incur that cost.
- Threads are less isolated: a thread’s segmentation fault kills its process and all its threads, which is one reason browsers isolate tabs in separate processes.
24:24
Pthreads Setup and the `pthread_create` Interface
- POSIX threads are used through `<pthread.h>`; compile and link with `-pthread`, or enable the corresponding Meson threads dependency.
- Pthread functions generally return `0` on success and an error number directly on failure; `strerror` can convert that number to a message.
- `pthread_create` takes a `pthread_t *` output, attributes (often `NULL`), a start-routine function pointer, and one `void *` argument.
- The new thread runs the supplied start routine when scheduled; the calling thread continues after `pthread_create`, so creation does not guarantee which thread runs first.
30:10
Scheduling Order and Why Returning from `main` Matters
- A program that prints once in `main` and once in a worker can show different output orders because the kernel chooses when each thread runs.
- If `main` returns first, the process exits and terminates its other threads before they necessarily print.
- Concurrent execution on one core is sufficient to explain nondeterministic output; multiple cores add parallel execution and can interrupt work at even finer points.
36:50
Joining Threads and Reclaiming Their Results
- `pthread_join(thread, NULL)` blocks until the specified joinable thread terminates and discards its return value.
- A terminated joinable thread that has not been joined remains a zombie thread, retaining resources until its termination is acknowledged.
- A thread can be joined exactly once; unlike `wait`, `pthread_join` requires naming the particular thread because threads have no parent-child relationship.
- Joining the worker ensures its output completes before the main thread exits the process.
43:00
Linux `clone` and the Different Meanings of Thread Exit
- On Linux, `pthread_create` ultimately uses `clone` or `clone3` with flags that share resources such as virtual memory, file descriptors, and signal handlers.
- `strace -f` can reveal these calls, but its displayed IDs for threads should not be confused with distinct process IDs.
- Returning from a thread’s start routine invokes `pthread_exit`; returning from `main` invokes process-wide exit and ends all threads.
- Calling `pthread_exit` in the main thread lets other threads continue, and the process ends when its last thread exits; any thread can still call `exit` to terminate the whole process.
49:00
Detached Threads and Pthread Stack Attributes
- `pthread_detach` marks a thread so its resources are released automatically on termination; a detached thread cannot be joined or have its return value collected.
- Use detached threads only when the program does not need to wait for them or retrieve their results; detaching an already detached thread is undefined behavior.
- Pthread attributes can configure stack size and detach state before creation; initialize and destroy the opaque attribute object with its accessor functions.
- On Linux, the default thread stack is typically 8 MiB and has a guard page.
54:10
Creating Four Workers and Passing Each an ID
- The example starts four threads with IDs 1 through 4; each worker prints its ID while counting from 0 through 9.
- Each worker receives a `void *` argument that it casts back to `int *`, reads, and frees after use.
- The workers’ output order is not guaranteed, even though each individual worker executes its own loop in order.
- The example allocates each ID on the heap so its value remains valid after the function that creates the thread returns.
1:00:00
Why Passing a Stack Variable’s Address Breaks
- Passing `&id` from a creator function’s stack frame is unsafe if the worker reads it after that function returns.
- The main thread may reuse the same stack location for later loop iterations, so workers can read a later ID instead of their intended value.
- Heap-allocated argument storage gives each worker a stable value; the worker frees that allocation when finished.
- A useful rule of thumb is not to pass a stack variable’s address to work that may outlive the current function.
1:04:50
Why Casting Integer IDs to Pointers Is Not Portable
- Casting an integer ID directly to `void *` may appear to work on a particular machine but depends on pointer width and byte ordering.
- The demonstration’s apparent success relies on the pointer being larger than an `int` and on little-endian layout; other architectures may behave differently.
- Compiler warnings about mismatched pointer and integer types identify a real portability problem, so use correctly typed, stable argument storage instead.
Summary, takeaways, and chapters were generated by AI from the video's transcript and may contain errors. The video belongs to its creator, Jon Eyolfson.