Thread

On this page 7

A thread is the smallest sequence of instructions that an operating system can schedule independently. Each thread belongs to a process and has its own program counter, registers and stack, while it shares the process’s address space, open files and other resources with every other thread of the same process. In concurrency and programming generally, threads are the basic mechanism that lets one program make progress on several tasks at once.

Because threads share memory, they communicate cheaply by reading and writing the same data, but that same sharing is the source of the classic concurrency hazards: race conditions, deadlocks and visibility problems. Understanding a thread therefore means understanding both what it gives, concurrent and potentially parallel execution, and what it demands, disciplined synchronization of shared state.

How a thread executes inside a process

A thread executes when the operating system scheduler assigns it to a CPU core, runs it for a time slice and later suspends it to run another thread. Switching between threads is a context switch: the kernel saves the registers and program counter of the outgoing thread and restores those of the incoming one.

The table shows which resources a thread owns privately and which it shares with the other threads of its process.

ResourcePrivate or sharedConsequence
Program counter and registersPrivateEach thread follows its own path through the code
Call stackPrivateLocal variables are safe from other threads by default
Thread-local storagePrivatePer-thread copies of selected variables
Heap and global variablesSharedData visible to all threads; needs synchronization
Open files and socketsSharedAny thread can use or close a descriptor
Address space and codeSharedCreating a thread is cheaper than creating a process
Private and shared resources of a thread

A thread moves through a small set of states: new, runnable, running, blocked or waiting, and terminated. A thread that waits for disk, network or a lock is blocked and consumes no CPU time, which is why threads let a program keep working while some tasks wait for input and output.

Kernel threads, user threads and virtual threads

Threads differ mainly in who schedules them and how much they cost. Most runtimes today map each language-level thread to one kernel thread, but several modern platforms add lightweight threads that the runtime multiplexes onto a small pool of kernel threads.

The following table compares the main thread models with typical characteristics; exact figures depend on the operating system, runtime and configuration.

ModelScheduled byTypical initial stackPractical scale
Kernel thread (1:1)Operating systemAbout 1 MB on Windows, often 8 MB of virtual space on LinuxThousands per process
Green thread (N:1)Language runtime on one kernel threadSmall, runtime-definedMany, but no parallelism
Goroutine (M:N)Go runtimeAbout 2 KB, grows on demandHundreds of thousands or more
Java virtual thread (M:N)JVM, since Java 21Small, heap-allocated stack chunksMillions in suitable workloads
Thread models compared

Stack memory for kernel threads is usually reserved as virtual address space and committed only as it is used, so the reserved size overstates real memory consumption. The practical limit comes from scheduler overhead, memory for actually used stacks and context-switch cost, which is typically in the range of one to several microseconds plus the indirect cost of cold CPU caches.

Threads compared with processes and asynchronous code

A thread is one of several ways to structure concurrent work, and the right choice depends on isolation needs and the type of workload. Processes give stronger isolation, while asynchronous programming handles very many waiting tasks with few threads.

The table contrasts threads with the main alternatives.

AspectThreadProcessAsynchronous task
MemoryShared within the processIsolated by defaultShared, usually on one event loop
Creation costModerateHighVery low
Failure impactA crash can end the whole processContained to one processAn unhandled error can stall the loop
Parallel executionYes, on multiple coresYesNot by itself
Best fitCPU-bound work with shared dataIsolation and fault containmentMany concurrent input and output waits
Threads, processes and asynchronous tasks

Threads provide concurrency, meaning overlapping lifetimes of tasks, and on multicore hardware they also provide parallelism, meaning simultaneous execution. A notable exception is standard CPython, where the global interpreter lock lets only one thread execute Python bytecode at a time; free-threaded builds without that lock are available in recent versions but are not yet the default.

Synchronization and thread safety

Code is thread-safe when it behaves correctly no matter how the scheduler interleaves the threads that call it. Achieving this requires controlling access to shared mutable state with synchronization primitives and respecting the memory model of the language, which defines when a write by one thread becomes visible to another.

  • Mutex or lock: allows only one thread at a time into a critical section.
  • Read-write lock: permits many concurrent readers or one exclusive writer.
  • Semaphore: limits the number of threads that may use a resource simultaneously.
  • Condition variable: lets a thread sleep until another thread signals a change.
  • Atomic operations: update a single variable indivisibly without a full lock.
  • Immutable data and message passing: avoid shared mutable state altogether.

Most production systems do not create a thread per task but use a thread pool, a fixed or bounded set of worker threads that take tasks from a queue. For CPU-bound work, a pool size close to the number of cores is a common starting point; for blocking work, larger pools help, but each additional thread adds memory and switching overhead.

Common threading bugs and how to prevent them

Threading bugs are hard to find because they depend on timing and are often not reproducible. A program can pass thousands of test runs and fail under production load, which makes prevention by design more effective than debugging after the fact.

The table lists the most frequent problems with their cause and a standard remedy.

ProblemCausePrevention
Race conditionUnsynchronized access to shared dataLocks, atomics or confinement of data to one thread
DeadlockThreads wait for each other’s locks in a cycleAcquire locks in a fixed global order; use timeouts
StarvationA thread never obtains the resource it needsFair locks and bounded waiting
Visibility errorA write stays in a cache or registerProper synchronization or volatile and atomic types
Thread leakThreads are created but never finishPools with bounded size and clear shutdown
Typical threading problems

The most reliable strategy is to minimize shared mutable state. Data that only one thread touches needs no locking, and immutable data can be shared freely; locks should then protect the small remainder and be held for as short a time as possible.

The short version

A thread is like one cook in a shared kitchen. A restaurant can have several cooks working at the same time, each following a different recipe, but they all use the same fridge, counters and pans.

That sharing makes the kitchen efficient, because nobody needs a separate set of equipment. It also creates conflicts: two cooks grabbing the same pan at once can ruin both dishes. Programs with several threads face the same problem, so they use rules about who may touch what and when. The key point is that threads let a program do several things at once, as long as the shared work is carefully coordinated.

Where it makes a difference

A desktop photo editor applies a filter to a 40-megapixel image. In the first version, the filter runs on the same thread that draws the user interface, so the window freezes for about eight seconds and the operating system marks the application as not responding.

The developers move the filter to a background worker thread and split the image into horizontal strips processed by a thread pool with one worker per core. On an eight-core laptop the filter finishes in roughly one and a half seconds rather than eight, short of a perfect eightfold speedup because of memory bandwidth and coordination overhead. The interface thread stays free to show a progress bar and a cancel button.

The only shared data is the output image, and each worker writes to its own strip, so no locks are needed for the pixel data. A single atomic counter tracks completed strips for the progress bar. The result is a responsive application and a roughly fivefold faster filter.