Skip to content

Threads and Concurrency in Operating Systems

Operating Systems Operating Systems 7 min read 1324 words Beginner ExcellentWiki Editorial Team

Threads enable multiple execution paths within a single process. They share memory but must coordinate access to shared data. Understanding the thread model your operating system provides is essential for writing correct and efficient concurrent programs.

Processes vs. Threads

FeatureProcessThread
Address spaceSeparate (isolated)Shared with process
Context switch overheadHigh (TLB flush, page tables)Low (registers only)
Creation timeSlow (fork + exec)Fast (clone)
CommunicationIPC (pipes, sockets, shared mem)Shared memory (direct)
Fault isolationYes (process crash)No (one thread crash = all crash)
SecurityStrong (separate address space)Weak (same address space)

Process isolation is the operating system’s strongest security boundary. A crash in one process cannot corrupt another process’s memory, and an unprivileged process cannot read another process’s data. Threads sacrifice this isolation for performance — context switching between threads is orders of magnitude faster because the TLB and page tables remain unchanged.

Thread Models

User-Level Threads (N:1)

Many user threads mapped to one kernel thread.

User space:   T1  T2  T3  T4    (user threads)
                  |
Kernel:           P             (single process/thread)
ProsCons
Fast context switch (no syscall)Blocking one thread blocks all
Portable (no kernel support needed)Can’t use multiple CPUs
Custom schedulingPage faults block everything

Kernel-Level Threads (1:1)

Each user thread mapped to a kernel thread.

User space:   T1    T2    T3    (user threads)
              |     |     |
Kernel:       KT1   KT2   KT3   (kernel threads)
ProsCons
True parallelism (multiple CPUs)Slower context switch (syscall)
Blocking one thread doesn’t block othersKernel resources per thread
Kernel handles schedulingLess control

Hybrid (M:N)

Many user threads multiplexed on fewer kernel threads.

User space:   T1  T2  T3  T4  T5  T6   (user threads)
               \  /     \  /     \  /
Kernel:        KT1      KT2      KT3    (kernel threads)

Used in: Go (goroutines), Java (green threads).

Go’s goroutines are the most successful M:N implementation in widespread use. The Go runtime schedules millions of goroutines across a small pool of kernel threads, multiplexing them with cooperative sharing at yield points and preemption at safe points.

Thread States

New → Ready ↔ Running → Blocked → Terminated
      ↑         │
      └─────────┘
      (Preempted or yield)
StateDescription
NewCreated but not started
ReadyWaiting for CPU
RunningExecuting on CPU
BlockedWaiting for I/O, lock, or condition
TerminatedFinished execution

POSIX Threads (pthreads)

Standard threading API in Unix-like systems.

Creating Threads

#include <pthread.h>

void *worker(void *arg) {
    int id = *(int *)arg;
    printf("Thread %d running\n", id);
    return NULL;
---

int main() {
    pthread_t threads[4];
    int ids[4] = {0, 1, 2, 3};

    for (int i = 0; i < 4; i++) {
        pthread_create(&threads[i], NULL, worker, &ids[i]);
    }

    for (int i = 0; i < 4; i++) {
        pthread_join(threads[i], NULL);
    }
    return 0;
---

Mutex

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void *safe_increment(void *arg) {
    int *counter = (int *)arg;
    pthread_mutex_lock(&lock);
    (*counter)++;  // Critical section
    pthread_mutex_unlock(&lock);
    return NULL;
---

Condition Variables

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

// Consumer
void *consumer(void *arg) {
    pthread_mutex_lock(&lock);
    while (!ready) {
        pthread_cond_wait(&cond, &lock);  // Release lock, wait
    }
    // ready == true, proceed
    pthread_mutex_unlock(&lock);
    return NULL;
---

// Producer
void *producer(void *arg) {
    pthread_mutex_lock(&lock);
    ready = 1;
    pthread_cond_signal(&cond);  // Wake one consumer
    pthread_mutex_unlock(&lock);
    return NULL;
---

Thread Pools

Creating and destroying threads is expensive. Pre-create a pool:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService pool = Executors.newFixedThreadPool(4);

for (Task task : tasks) {
    pool.submit(() -> processTask(task));
---

pool.shutdown();
Thread PoolUse Case
Fixed thread poolKnown workload, bounded resources
Cached thread poolMany short-lived tasks
Single threadSerial execution
Work-stealing (ForkJoin)Divide-and-conquer

Work-stealing thread pools, like Java’s ForkJoinPool, allow idle threads to steal tasks from busy threads’ queues. This is particularly effective for divide-and-conquer algorithms where subproblems may have very different sizes, as it keeps all cores busy even when the workload distribution is uneven.

Thread Safety Strategies

StrategyDescriptionPerformance
Mutual exclusionLock shared dataModerate (contention)
Atomic operationsLock-free CASFast
Thread-local storageNo sharing neededFastest
Immutable dataDon’t modify, copyDepends on copy cost
Message passingSend data, don’t shareGood (no contention)

Lock-Free Programming with Atomic Operations

#include <stdatomic.h>

atomic_int counter = 0;

void *safe_increment(void *arg) {
    atomic_fetch_add(&counter, 1);  // Atomic, no lock
    return NULL;
---

The ABA Problem

Lock-free data structures using compare-and-swap (CAS) must handle the ABA problem: a pointer changes from A to B and back to A between the read and the CAS, causing the CAS to succeed when it should not. Solutions include tagged pointers (storing a version counter alongside the pointer) and double-word CAS that atomically updates both the pointer and a tag.

Thread Pools in Different Languages

Python (ThreadPoolExecutor)

from concurrent.futures import ThreadPoolExecutor

def process_item(item):
    return item * 2

with ThreadPoolExecutor(max_workers=4) as pool:
    results = list(pool.map(process_item, range(100)))

Note that Python’s Global Interpreter Lock (GIL) prevents true parallelism for CPU-bound work within a single process. ThreadPoolExecutor is best for I/O-bound tasks. For CPU parallelism in Python, use ProcessPoolExecutor or write performance-critical sections in C extensions.

Go (Goroutines)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2
    }
---

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    
    for w := 1; w <= 4; w++ {
        go worker(w, jobs, results)
    }
    
    for j := 1; j <= 100; j++ {
        jobs <- j
    }
    close(jobs)
---

Rust (std::thread)

use std::thread;

let handles: Vec<_> = (0..4).map(|i| {
    thread::spawn(move || {
        println!("Thread {} running", i);
    })
---).collect();

for handle in handles {
    handle.join().unwrap();
---

Rust’s ownership system enforces thread safety at compile time. The Send and Sync traits determine which types can be safely transferred and shared across threads, eliminating data races without a garbage collector.

Thread Debugging

ToolUse
GDBThread backtrace, breakpoints
Helgrind (Valgrind)Race condition detection
ThreadSanitizerFast data race detection
straceSystem call tracing
perfPerformance profiling

Detecting Data Races

Data races occur when two threads access the same memory location without synchronization, and at least one access is a write. ThreadSanitizer (TSan) instruments every memory access at compile time and detects races at runtime. Integrate TSan into your test suite — it has near-zero false positives and catches races that would take weeks to reproduce manually.

Capability vs. Performance Tradeoffs

Each thread model makes different tradeoffs. User-level threads offer fast creation and context switching but cannot exploit multiple cores. Kernel threads provide true parallelism but at higher overhead per thread and context switch. The optimal choice depends on whether your workload is CPU-bound, I/O-bound, or mixed, and whether you need fine-grained control over scheduling or can rely on the OS scheduler.

Threads make programs faster — or much harder to debug. Always use the simplest concurrency model that works.

FAQ

What is the difference between concurrency and parallelism? Concurrency is about dealing with multiple tasks at once (structuring a program to handle overlapping tasks). Parallelism is about executing multiple tasks simultaneously (using multiple CPU cores). Concurrency enables parallelism, but they are distinct concepts.

Why are threads lighter than processes? Threads share the same address space, file descriptors, and other kernel resources with their parent process. Creating a thread does not require duplicating the entire address space — only a new stack and thread-local storage are allocated. Context switching between threads is faster because the virtual memory configuration stays the same.

What causes a race condition? A race condition occurs when two or more threads access shared data without proper synchronization, and the result depends on the order of execution. Race conditions are non-deterministic bugs that may only manifest under specific timing conditions, making them extremely difficult to reproduce and debug.

How many threads should I use? For CPU-bound work, use as many threads as there are CPU cores. For I/O-bound work, you can use more threads — often many more — because most threads will be waiting on I/O at any given time. Profile your application to find the optimal thread count rather than guessing.

Synchronization GuideDeadlocks GuideProcess Scheduling Guide

Section: Operating Systems 1324 words 7 min read Beginner 756 articles in section Report inaccuracy Back to top