Threads
1049 words
5 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# Threads ## 🎯 Learning Objectives - Differentiate between a process and a thread - Compare user-level vs kernel-level threads - Explain the three threading models (many-to-one, one-to-one, many-to-many) - Write multithreaded programs using the pthreads API - Understand thread pools and their benefits * * * ## 1. T...

Threads
🎯 Learning Objectives
- Differentiate between a process and a thread
- Compare user-level vs kernel-level threads
- Explain the three threading models (many-to-one, one-to-one, many-to-many)
- Write multithreaded programs using the pthreads API
- Understand thread pools and their benefits
1. Thread Concepts
1.1 Intuition
A thread is a lightweight process — the smallest unit of CPU utilization. A process can have multiple threads that share the same address space (code, data, heap) but each has its own stack and register set. Think of a process as a house with multiple rooms (threads) — they share the kitchen and living room (heap/data) but each has their own bedroom (stack).
1.2 Process vs Thread
(Diagram)
| Aspect | Process | Thread |
|---|---|---|
| Address space | Private (separate per process) | Shared with other threads in the same process |
| Creation | fork() — expensive (copy address space) | pthread_create() — cheap (share address space) |
| Context switch | Slow (page table switch, TLB flush) | Fast (same address space) |
| Communication | IPC (pipes, sockets, shared memory) | Direct (shared global variables) |
| Independence | Fully independent (one crash doesn't affect others) | Dependent (one thread crash can crash all) |
2. Threading Models
(Diagram)
| Model | Description | Pros | Cons | Examples |
|---|---|---|---|---|
| Many-to-One | Many user threads → 1 kernel thread | Efficient context switch (user space) | Blocking one blocks all | Green threads (Solaris) |
| One-to-One | 1 user thread → 1 kernel thread | True parallelism (multicore) | Overhead (creating kernel threads) | Linux (NPTL), Windows, macOS |
| Many-to-Many | M user threads → N kernel threads | Best of both (flexibility + parallelism) | Complex implementation | Solaris (before v9) |
2.1 User-Level Threads (ULT)
- Managed entirely in user space (no kernel involvement)
- Thread library handles scheduling (e.g., GNU Pth)
- Pros: Fast creation/context switch, no kernel modification needed
- Cons: Blocking one thread blocks all (kernel sees one process), no true parallelism
2.2 Kernel-Level Threads (KLT)
- Managed by the kernel (OS scheduler)
- Pros: True parallelism (scheduled independently on different cores), one thread blocks independently
- Cons: Slower creation/context switch (system calls), more overhead
3. pthreads API
POSIX threads (pthreads) is the standard threading API on Unix-like systems.
3.1 Basic pthread Functions
c#include <pthread.h> // Create a thread int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); // Wait for a thread to finish int pthread_join(pthread_t thread, void **retval); // Exit current thread void pthread_exit(void *retval); // Get own thread ID pthread_t pthread_self(void); // Compare thread IDs int pthread_equal(pthread_t t1, pthread_t t2);
3.2 Worked Example: Multithreaded Sum
c#include <stdio.h> #include <pthread.h> #define NUM_THREADS 4 #define ARRAY_SIZE 1000 int arr[ARRAY_SIZE]; int partial_sums[NUM_THREADS] = {0}; void* sum_partial(void* arg) { int thread_id = *(int*)arg; int start = thread_id * (ARRAY_SIZE / NUM_THREADS); int end = start + (ARRAY_SIZE / NUM_THREADS); for (int i = start; i < end; i++) { partial_sums[thread_id] += arr[i]; } printf("Thread %d: partial sum = %d\n", thread_id, partial_sums[thread_id]); pthread_exit(NULL); } int main() { pthread_t threads[NUM_THREADS]; int thread_ids[NUM_THREADS]; int total_sum = 0; // Initialize array for (int i = 0; i < ARRAY_SIZE; i++) arr[i] = i + 1; // Create threads for (int i = 0; i < NUM_THREADS; i++) { thread_ids[i] = i; pthread_create(&threads[i], NULL, sum_partial, &thread_ids[i]); } // Wait for all threads for (int i = 0; i < NUM_THREADS; i++) { pthread_join(threads[i], NULL); total_sum += partial_sums[i]; } printf("Total sum = %d (expected %d)\n", total_sum, 1000*1001/2); return 0; }
Output:
pseudoThread 0: partial sum = 31375 Thread 1: partial sum = 93875 Thread 2: partial sum = 156375 Thread 3: partial sum = 218875 Total sum = 500500 (expected 500500)
4. Thread Pools
A thread pool creates a fixed number of threads at startup and reuses them for multiple tasks.
(Diagram)
Benefits:
- Avoids thread creation overhead (amortized across tasks)
- Controls resource usage (max threads)
- Natural load balancing
5. Common Pitfalls
Pitfall 1: Data races (unsynchronized shared data)
cint counter = 0; void* increment(void* arg) { for (int i = 0; i < 100000; i++) counter++; // Race condition! }
Fix: Use mutex locks or atomic operations.
Pitfall 2: Deadlock with multiple mutexes
Mistake: Thread A locks L1 then L2, Thread B locks L2 then L1.
Fix: Establish a fixed lock ordering (always lock in the same sequence).
Pitfall 3: Memory leaks from detached threads
Mistake: Creating detached threads and losing the handle → can't join, resources leaked.
Fix: Keep track of all threads; either join or properly detach.
6. 📐 Key Formulas / Concepts
| Concept | Description | Key Insight |
|---|---|---|
| Thread | Lightweight process unit | Shares address space, private stack |
| pthread_create | Create a thread | Takes function pointer and argument |
| pthread_join | Wait for thread | Collects return value |
| Thread pool | Reusable thread set | Amortizes creation overhead |
| Data race | Unsynchronized concurrent access | Use mutexes to protect shared data |
7. 📝 Practice Questions
Q1: How many threads does fork() create? How many does pthread_create() create?Answer:fork()creates one new process (which starts with a single thread).pthread_create()creates one new thread within the existing process (sharing address space). Q2: In the one-to-one threading model, what happens when a thread makes a blocking I/O call?Answer: The kernel blocks only that specific kernel thread. Other threads (both user and kernel) continue running independently. This is a key advantage over many-to-one, where the entire process would block. Q3: Why is context switching between threads of the same process faster than between processes?Answer: Threads share the same address space, so there's no need to switch page tables or flush the TLB. Only registers and stack pointers need to be saved/restored. Q4: What is the purpose of pthread_join()?Answer: It causes the calling thread to block until the target thread terminates. It also collects the return value (if any) and cleans up the thread's resources. Q5: Explain the difference between user-level and kernel-level threads.Answer: User-level threads are managed by a thread library in user space without kernel involvement — fast creation but no true parallelism. Kernel-level threads are managed by the OS kernel — true parallelism but higher overhead. User-level threads cannot take advantage of multiple cores.
8. 🔗 Cross-References
- Week 1 - Process Management: Compare process vs thread creation
- Week 5 - Synchronization: Mutexes and condition variables for thread safety
- BSCS3005 (C Programming): C function pointers used in pthread_create Join Discord PreviousOS Structures & BootNextxv6 Booting & System Calls