Skip to content
Fundamentals

Processes and Threads: What They Are, Where They Came From and How They Work

A process is a running program with its own private world; a thread is one line of execution inside it. Here is what each one is, how the ideas developed from 1960s time-sharing to goroutines and virtual threads, and how operating systems create, schedule and coordinate them.

What Is an Operating System? introduced processes and threads in a few paragraphs, and What Is a Processor? showed the hardware they run on. This article gives them the space they deserve. Almost every question about how a program behaves when it does more than one thing at a time, from “why did my server run out of memory?” to “why does this counter come out wrong?”, comes down to the difference between a process and a thread.

What they are

A process is a running program together with everything it owns: a private address space, open files and network connections, and an identity. A thread is one sequence of execution inside a process: a program counter, a set of registers and a stack. Every process has at least one thread; a multithreaded process has several, all sharing the process’s memory and resources.

The distinction is between ownership and execution. The process is the container that the operating system protects and accounts for; the thread is what the scheduler actually puts on a CPU core.

A process with three threads. The code, global data, heap and resources such as open files belong to the process and are shared by all its threads. Each thread has its own registers and program counter, its own stack and its own thread-local storage.Processone address space, one set of resourcesShared by every threadCodemachine instructionsDataglobal variablesHeapmalloc, newResourcesfiles, sockets, PID, userPrivate to each threadThread 1Registers, PCStackthread-local storageThread 2Registers, PCStackthread-local storageThread 3Registers, PCStackthread-local storage
A process with three threads. Everything in the top row is shared; each thread has only its own registers, stack and thread-local storage.
ProcessThread
What it isA program in execution, with its own resourcesOne flow of control within a process
MemoryIts own private address spaceShares its process’s address space
OwnsAddress space, open files, sockets, user and group identity, signal handlersRegisters, program counter, stack, thread-local storage
IsolationStrong: one process cannot read or corrupt another’s memory without the OS’s helpNone from its sibling threads: any thread can read or overwrite any shared data
Cost to createHigher: a new address space and resource tablesLower: a new stack and register set
CommunicationThrough the OS: pipes, sockets, shared-memory segments, signalsDirectly through shared variables, guarded by locks
If it crashesUsually only that process diesUsually the whole process dies with it

Why have both

One thread per process was the original model, and it is still the simplest: each process is isolated, and nothing inside it happens at the same time as anything else. Threads were added because many programs want to do several things at once on the same data:

  • Use several cores. A single thread can run on only one core at a time. A computation split across threads can use them all.
  • Stay responsive. An editor can keep accepting keystrokes in one thread while another saves the file or checks spelling.
  • Overlap waiting. A server can wait on one client’s slow network connection in one thread while others serve everyone else.
  • Share cheaply. Threads exchange data simply by reading and writing the same memory, with no copying through the operating system.

The cost of that sharing is the subject of half this article: when several threads can touch the same data, the programmer has to coordinate them.

TermMeaning
ProgramThe executable file on disk. One program can be running as many processes at once
TaskUsed loosely; inside the Linux kernel, the single structure that represents both processes and threads
Kernel threadA thread the operating system kernel knows about and schedules directly
User-level (green) threadA thread managed by a library or language runtime inside a process, invisible to the kernel
Fiber, coroutineA unit of execution that switches only where the code explicitly yields, never preemptively
JobA group of processes treated as one, such as a shell pipeline or a Windows job object

A history of processes and threads

The 1960s: the process is named

The idea of a process grew out of multiprogramming and time-sharing. Once a computer switched between several programs, its designers needed a name for “a program as it is being run”, distinct from the program text and from the processor running it. Jack Dennis and Earl Van Horn gave the classic definition in 1966: a process is a locus of control within an instruction sequence, the abstract entity that moves through a program’s instructions as a processor executes them.1 In the same years, Melvin Conway proposed the fork and join operations for splitting a computation into parallel paths and waiting for them to finish,2 and Edsger Dijkstra’s Cooperating Sequential Processes introduced semaphores to coordinate processes that share data.3

Unix: fork, exec and the pipe

Unix (1969 onwards) made processes cheap and composable. A process is created with fork, which duplicates the caller, and given a new program with exec; the shell, pipes and redirection are all built from those two calls.4 Dennis Ritchie later noted that the split between fork and exec was not new: it was already present in the Berkeley time-sharing system of Project Genie, which Ken Thompson knew well.5 Each Unix process had exactly one thread of control, and for most of the 1970s that was the normal model.

The 1980s and early 1990s: threads arrive

As shared-memory multiprocessors appeared, a process with one thread became too coarse. Several systems split the process in two. Mach (Carnegie Mellon, from 1985) separated the task, which owns an address space and resources, from the thread, the unit of execution, and allowed many threads per task.6 Windows NT (1993) was designed from the start with processes that contain one or more kernel-scheduled threads.78 Sun’s Solaris 2 used a two-level design that multiplexed user threads over kernel “lightweight processes”; from Solaris 9 (2002) every thread got its own lightweight process.910

Researchers argued over where threads should live. User-level thread libraries were fast but invisible to the kernel, so one thread blocking in a system call stopped them all; kernel threads were safe but slower to create and switch. Anderson and colleagues proposed scheduler activations (1991) as a way for the kernel and a user-level library to cooperate.11 The portable programming interface was settled in 1995 by POSIX threads (Pthreads, IEEE Std 1003.1c).12

Linux: one structure for both

Linux took an unusual route. Instead of separate process and thread objects, it has one kernel structure, the task, and a system call, clone, that creates a new task and lets the caller choose what to share with it: memory, open files, signal handlers and so on.13 Share nothing and you have a new process; share everything and you have a new thread. The first Pthreads library built on this, LinuxThreads (1996), had awkward corners, such as each thread showing up with its own process ID.14 The Native POSIX Thread Library (NPTL), written by Ulrich Drepper and Ingo Molnar, fixed them. It became part of glibc from version 2.3.2 and relies on support added in the Linux 2.6 kernel (2003) for thread groups and for fast user-space locks called futexes.151614

Threads under criticism, and events

Threads made concurrency easy to start and hard to get right. In 1996 John Ousterhout gave a widely cited talk arguing that threads were a bad idea for most purposes and that event-driven programming was simpler.17 In 1999 Dan Kegel’s The C10K problem asked how one server could handle ten thousand simultaneous connections, when a thread per connection meant ten thousand stacks.18 The answer that spread was the event loop: one thread waiting on many connections through epoll or kqueue, as in nginx and later Node.js.1920 In 2006 Edward Lee argued that threads were a fundamentally flawed model of concurrency because they make every shared variable a potential bug.21 Others had already defended them, arguing that the problems were in particular implementations, not the model.22

Meanwhile the hardware turned against single threads. Once clock speeds stalled in the mid-2000s, more performance meant more cores, and more cores meant more threads. Languages also had to define precisely what a thread sees when another writes to memory: Java’s memory model was revised in 2004, and C and C++ gained one in 2011.232425

Lightweight threads return

The latest turn reconciles the two camps. Erlang (from 1986) had always run huge numbers of tiny, isolated processes inside one operating system process, scheduled by its runtime.26 Go (2009) made goroutines the everyday unit of concurrency: cheap enough to start hundreds of thousands, and multiplexed by the Go runtime over a small set of OS threads.2728 Java added virtual threads in Java 21 (2023), which let ordinary blocking code scale like an event loop.29 Many languages took a third path with async/await, which writes event-loop code in a sequential style.

A timeline at a glance

YearMilestone
1963Conway proposes fork and join
1965Dijkstra’s Cooperating Sequential Processes and semaphores
1966Dennis and Van Horn define the process
1969–74Unix: processes with fork and exec, then pipes
1974Hoare’s monitors and condition variables
1985Mach separates tasks from threads
1986Erlang
1991Scheduler activations
1993Windows NT: kernel threads from the start
1995POSIX threads standard
1996Ousterhout’s Why Threads Are a Bad Idea; LinuxThreads
1999The C10K problem
2003NPTL in Linux 2.6
2004–11Memory models for Java (2004), C and C++ (2011)
2009Go and goroutines
2023Virtual threads in Java 21

Models

The process life cycle

The kernel tracks what each process (or, on Linux, each task) is doing. On Linux, ps shows it as a one-letter state:30

CodeStateMeaning
RRunning or runnableOn a CPU, or ready and waiting for one
SInterruptible sleepWaiting for an event, such as input or a timer; a signal can wake it
DUninterruptible sleepWaiting inside the kernel, usually for disk I/O; cannot be interrupted
TStoppedPaused by a job-control signal such as Ctrl+Z (a process stopped under a debugger shows as lowercase t)
ZZombieFinished, but its parent has not yet collected its exit status

On Unix-like systems processes form a tree. Every process except the first has a parent, the process that created it, and each is identified by a process ID (PID).

The life of a child process on Unix. The parent calls fork, creating a child that shares the parent's memory copy-on-write, and carries on with its own work. The child calls execve to replace its program with ls, which runs and then exits. The finished child remains a zombie until the parent calls waitpid and collects its exit status.Parentfork()carries on with its own workwaitpid()Childexecve("/bin/ls")copy of the parent,copy-on-writenew program runsexit(0)zombieexit statuscollected
Creating, running and reaping a child process on Unix. The zombie stage lasts only until the parent calls wait; a parent that is already waiting reaps the child at once.
  1. Creation. fork creates a child that is a copy of the parent: same program, same memory contents, same open files. Copying is lazy: parent and child share every page copy-on-write, so only pages one of them modifies are actually copied.31
  2. New program. The child calls execve, which replaces its whole address space with a new program while keeping its PID and, unless marked otherwise, its open files. This is how a shell connects the child’s standard input and output to a file or a pipe before the new program starts.
  3. Termination. The child calls exit (or is killed by a signal). The kernel frees its memory but keeps a small record, its exit status, so the parent can learn how it ended. Until then it is a zombie.
  4. Reaping. The parent calls wait or waitpid, collects the status, and the zombie disappears. If the parent dies first, the orphan is adopted by init (process 1) or a designated “subreaper”, which reaps it.32

Here is the shell pipeline ls | wc -l built by hand from these calls:

c
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

/* The equivalent of the shell pipeline  ls | wc -l  */
int main(void) {
    int fd[2];
    pipe(fd);                          /* fd[0] is the read end, fd[1] the write end */

    if (fork() == 0) {                 /* first child: ls */
        dup2(fd[1], STDOUT_FILENO);    /* its output goes into the pipe */
        close(fd[0]); close(fd[1]);
        execlp("ls", "ls", (char *)NULL);
        _exit(127);                    /* reached only if exec failed */
    }
    if (fork() == 0) {                 /* second child: wc -l */
        dup2(fd[0], STDIN_FILENO);     /* its input comes from the pipe */
        close(fd[0]); close(fd[1]);
        execlp("wc", "wc", "-l", (char *)NULL);
        _exit(127);
    }
    close(fd[0]); close(fd[1]);        /* the parent uses neither end */
    while (wait(NULL) > 0)             /* reap both children */
        ;
    return 0;
}

Not every system works this way. The Win32 API has no fork: Windows creates a process and loads its program in one step with CreateProcess.33 POSIX offers posix_spawn for the same one-step pattern, and some researchers argue that fork has become a liability: it is slow for large processes, interacts badly with threads, and complicates the kernel.34

Threading models

There are three ways to map the threads a program creates onto the threads the kernel schedules:

Three threading models. One-to-one: each user thread is a kernel thread, as on Linux, Windows and macOS. Many-to-one: all user threads of a process share one kernel thread, so one blocking call stops them all. Many-to-many: a runtime multiplexes many user threads over a smaller pool of kernel threads, as Go and Java virtual threads do.One-to-oneLinux, Windows, macOS threadsuser threadskernel threadsthe kernel schedules every threadMany-to-oneearly Java green threads, GNU Pthuser threadsone kernel threadone blocking call stops them allMany-to-manyGo goroutines, Java virtual threadsuser threadsfewer kernel threadsa runtime multiplexes them
The three threading models. The dashed line separates user space (above) from the kernel (below).
ModelHow it worksStrengthWeaknessExamples
One-to-one (1:1)Every thread is a kernel threadReal parallelism; a blocking call blocks only its own threadEach thread costs a kernel structure and a full-size stackPthreads on Linux (NPTL), Windows threads, macOS
Many-to-one (N:1)All of a process’s threads run on one kernel thread, switched by a libraryVery cheap to create and switchNo parallelism; one blocking system call stops every threadEarly Java “green threads”,35 GNU Pth36
Many-to-many (M:N)A runtime multiplexes many user threads over a pool of kernel threadsCheap threads and parallelismComplex; the runtime must avoid blocking its kernel threadsGo, Erlang, Java virtual threads, Solaris before version 9

General-purpose operating systems have converged on 1:1 for their native threads, and M:N now lives in language runtimes, which know enough about the program to schedule it well.

Communicating between processes

Because processes do not share memory by default, the operating system provides inter-process communication (IPC):

MechanismWhat it carriesTypical use
PipeA one-way byte stream between related processesShell pipelines
Named pipe (FIFO)A byte stream with a name in the file systemUnrelated processes on one machine
Unix domain socketA two-way stream or messages, local onlyTalking to local daemons, such as Docker or a database
Network socketA two-way stream or messages across machinesEvery network service
Shared memoryA region mapped into several address spacesHigh-volume data, such as video frames
Message queueDiscrete messages with an orderDecoupling producers and consumers
SignalA small integer notification“Stop”, “reload your configuration”, “a child exited”

Shared memory is the fastest, and it brings back exactly the coordination problems of threads.

How they work

What the kernel stores

For every process, the kernel keeps a process control block; for every thread, its saved registers and scheduling state. In Linux both are the same task_struct, and threads of one process are simply tasks that share their memory map, file table and signal handlers and belong to one thread group. What user space calls a process ID is the thread group ID; each thread also has its own thread ID.13 You can see this from the shell:

bash
ps -eLf               # one line per thread: LWP is the thread ID, NLWP the thread count
ls /proc/self/task    # each thread of a process has its own directory

Each thread needs its own stack, and stacks are where much of a thread’s cost lies. On Linux a new Pthread’s stack defaults to the process’s stack size limit, commonly 8 MiB of reserved address space;37 on Windows the default is 1 MB.38 Memory is committed only as the stack is actually used, but the reservation is why tens of thousands of OS threads strain a system. Go instead starts goroutines with stacks of a few kilobytes,28 and Java keeps virtual-thread stacks as small objects on the heap; both grow on demand.29

Creating a thread

In C with POSIX threads, a thread is a function started with pthread_create and waited for with pthread_join. On Linux, pthread_create allocates a stack and calls clone (or its newer form, clone3) with flags that share the address space, file descriptors, file system information and signal handlers, and place the new task in the caller’s thread group.13 Creating a thread this way is much cheaper than creating a process, but still involves a system call and kernel data structures, which is what user-level threads avoid.

In Go, the same idea is a keyword. This starts a hundred thousand goroutines, which would be reckless with OS threads (it needs Go 1.22 or later, where each loop iteration gets its own i):

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	results := make([]int, 100_000)
	for i := range results {
		wg.Add(1)
		go func() { // a new goroutine, not a new OS thread
			defer wg.Done()
			results[i] = i * i
		}()
	}
	wg.Wait()
	fmt.Println(results[99_999]) // 9999800001
}

Context switching

When the kernel moves a CPU core from one thread to another, it performs a context switch.

A context switch on one CPU core. Thread A runs until a timer interrupt or system call enters the kernel. The kernel saves A's registers, the scheduler picks thread B, and the kernel restores B's registers and returns to user mode, where B runs. A is now either ready and waiting for a CPU, or asleep if it entered the kernel through a blocking call.Thread AKernelThread Brunningready, or asleep if it blockedinterrupt or system callsave Aschedulerestore Breturn to user modereadyrunningthe switch itself: microseconds; refilling caches afterwards can cost more
A context switch on one core. Between the two threads, the kernel saves one set of registers, runs the scheduler and restores the other.
  1. Control enters the kernel, through a timer interrupt (the running thread’s time slice is up), a system call that blocks (it is waiting for I/O or a lock), or an interrupt that wakes a more important thread.
  2. The kernel saves the running thread’s registers, including its stack pointer and program counter.
  3. The scheduler chooses the next thread to run.
  4. If the next thread belongs to a different process, the kernel switches the MMU to that process’s page tables. Switching between threads of the same process skips this step, which is one reason it is cheaper.
  5. The kernel restores the next thread’s registers and returns to user mode, where it continues exactly where it was last stopped.

The switch itself takes on the order of microseconds. Its indirect cost is often larger: the new thread finds the caches and TLB full of the previous thread’s data, and runs slowly until they warm up again.39 A user-level runtime can switch between its own threads without entering the kernel at all, which is how Go and Java virtual threads keep switches cheap.

Scheduling threads

The scheduler (described in What Is an Operating System?) schedules threads, not processes: a process with eight runnable threads can occupy eight cores at once. Two refinements matter for threaded programs:

  • Affinity. The scheduler prefers to keep a thread on the core it last ran on, where its data may still be in cache, and programs can pin threads to particular cores.
  • Simultaneous multithreading. A core with SMT (Intel’s Hyper-Threading) appears to the operating system as two (on some processors more) logical CPUs, so two threads can share one physical core and its execution units.

How much a program speeds up with more threads is limited by the part of it that cannot run in parallel. Gene Amdahl’s 1967 argument, now called Amdahl’s law, says that if a fraction s of the work is serial, no number of processors can make the program more than 1/s times faster.40 A program that is 10 percent serial will never run more than ten times faster, however many cores it gets.

Synchronisation

Threads share memory, so whenever two of them access the same data and at least one writes, the program must control the order. Otherwise it has a data race, and the result depends on how the scheduler happened to interleave the threads, which changes from run to run.

The building blocks, all ultimately built on atomic instructions in the processor, are:

ToolWhat it guaranteesTypical use
Atomic operationA single read-modify-write (increment, compare-and-swap) happens indivisiblyCounters, flags, lock-free data structures
Mutex (lock)Only one thread at a time holds itProtecting a data structure
Read-write lockMany readers or one writerData that is read far more than written
Condition variableLets a thread sleep, with a mutex released, until another signals that something changedWaiting for a queue to become non-empty
SemaphoreA counter that threads decrement (waiting at zero) and incrementLimiting how many threads use a resource3
BarrierEvery thread waits until all have arrivedPhases of a parallel computation

Condition variables come from monitors, developed by Per Brinch Hansen and Tony Hoare (1974), which bundle data with the procedures allowed to touch it and let a procedure wait inside until a condition holds.41 The classic use is a bounded buffer between a producer and a consumer:

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

#define SIZE 8
static int buf[SIZE], count = 0, head = 0, tail = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
static pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;

static void put(int v) {
    pthread_mutex_lock(&lock);
    while (count == SIZE)                     /* re-check after every wake-up */
        pthread_cond_wait(&not_full, &lock);  /* sleeps with the lock released */
    buf[tail] = v; tail = (tail + 1) % SIZE; count++;
    pthread_cond_signal(&not_empty);
    pthread_mutex_unlock(&lock);
}

static int get(void) {
    pthread_mutex_lock(&lock);
    while (count == 0)
        pthread_cond_wait(&not_empty, &lock);
    int v = buf[head]; head = (head + 1) % SIZE; count--;
    pthread_cond_signal(&not_full);
    pthread_mutex_unlock(&lock);
    return v;
}

static void *producer(void *arg) {
    (void)arg;
    for (int i = 1; i <= 100; i++)
        put(i);
    put(-1);                                  /* tell the consumer to stop */
    return NULL;
}

int main(void) {
    pthread_t t;
    pthread_create(&t, NULL, producer, NULL);
    long sum = 0;
    for (int v; (v = get()) != -1; )
        sum += v;
    pthread_join(t, NULL);
    printf("%ld\n", sum);                     /* 5050 */
    return 0;
}

The while loops matter: a woken thread must check its condition again, because another thread may have got there first, and POSIX allows spurious wake-ups.42

Locks are made fast by keeping the common case out of the kernel. A Linux futex is an integer in shared memory that threads update with atomic instructions; only when a thread actually has to wait, or wake a waiter, does it make a system call.16 An uncontended lock therefore needs only an atomic instruction, with no system call at all.

Locks have their own failure modes. A deadlock happens when threads wait for each other in a cycle; the usual cure is to always take locks in the same order. Priority inversion happens when a low-priority thread holding a lock is starved by medium-priority work while a high-priority thread waits for that lock. Contention happens when so many threads want the same lock that they spend their time queuing rather than working.

Memory models

Modern processors and compilers reorder memory operations for speed. A single thread cannot tell, but another thread can. Leslie Lamport defined the intuitive ideal in 1979 as sequential consistency: the result is as if all threads’ operations ran in some single interleaved order, each thread’s in program order.43 Real hardware does not guarantee it, so languages specify a memory model: a contract saying which reorderings are allowed and which operations (locks, atomics, volatile in Java) restore order. Hans Boehm showed in 2005 that threads cannot be added correctly to a language purely as a library, because the compiler has to know the rules too.44 Java’s memory model had been rebuilt in 2004, and C and C++ adopted one in 2011, which Boehm’s work helped shape.232425 The practical rule they give programmers: a program without data races behaves as if it were sequentially consistent.

Signals and threads

Signals predate threads, and the combination is awkward. A signal sent to a process is delivered to any one of its threads that has not blocked it, so multithreaded programs usually block signals in every thread but one, which waits for them explicitly.45 Calling fork in a multithreaded program copies only the calling thread; any lock another thread held at that moment stays locked forever in the child, which is why a child of a threaded program should call exec almost immediately.31

Putting it together: serving many clients

How a network server handles many clients at once is a direct choice between the models in this article.

Three ways to structure a network server. Process per connection: each client is served by its own process. Thread per connection: one process, one thread per client. Event loop: one thread in one process waits on all connections at once through an event notification interface such as epoll and handles whichever is ready.Process per connectionApache preforkprocessprocessprocessisolated, but heavyThread per connectionApache worker, classic Java serversprocessshared memory, one stack eachEvent loopnginx, Node.js, Redisprocessone thread juggles all clients
Three classic server designs. The top circles are client connections; the small solid boxes are threads.
DesignHow it worksStrengthsWeaknesses
Process per connectionA new (or pre-forked) process serves each clientA crash or bug affects one client; simple codeMemory and creation cost per client; sharing state needs IPC
Thread per connectionOne thread per client, all in one processSimple blocking code; shared caches in memoryThousands of stacks; locking around shared state
Event loopOne thread waits on all connections with epoll or kqueue and handles whichever is readyTens of thousands of connections with little memoryCode split into callbacks; one slow handler stalls everyone; one core per loop
Lightweight threadsThread-per-connection code on goroutines or virtual threads, which the runtime parks on an event loop underneathSimple blocking code and event-loop scaleRequires a runtime that controls every blocking call

Real systems mix them. nginx runs one event loop per worker process, and is usually configured with one worker per CPU core.46 Web browsers went the other way for safety. Chrome ran web pages in separate renderer processes from its first release in 2008, by default giving each site instance (roughly, a group of related tabs on one site) its own process, so one crashing page did not take the others down.4748 Strict per-site isolation, including cross-site frames inside a page, became the default only with Site Isolation in Chrome 67 in 2018.49

Why it matters

Processes and threads are the two answers to the question “what is running?”, and they sit at opposite ends of a trade-off. Processes give isolation and pay for it in memory and communication. Threads give sharing and pay for it in coordination. Most of the bugs that are hardest to reproduce, such as races, deadlocks and memory corruption that appears only under load, come from treating threads as if they were processes. Many of the performance problems, such as servers with thousands of idle stacks and programs that do not speed up on more cores, come from treating processes or threads as free. Knowing what each one really costs, and what it really shares, is the start of reasoning about any concurrent program.

References


  1. Dennis, J. B., & Van Horn, E. C. (1966). Programming semantics for multiprogrammed computations. Communications of the ACM, 9(3), 143–155. https://doi.org/10.1145/365230.365252 ↩︎

  2. Conway, M. E. (1963). A multiprocessor system design. Proceedings of the November 12–14, 1963, Fall Joint Computer Conference, 139–146. https://doi.org/10.1145/1463822.1463838 ↩︎

  3. Dijkstra, E. W. (1965). Cooperating Sequential Processes (EWD 123). Department of Mathematics, Technological University, Eindhoven. https://www.cs.utexas.edu/~EWD/transcriptions/EWD01xx/EWD123.html ↩︎ ↩︎

  4. Ritchie, D. M., & Thompson, K. (1974). The UNIX time-sharing system. Communications of the ACM, 17(7), 365–375. https://doi.org/10.1145/361011.361061 ↩︎

  5. Ritchie, D. M. (1984). The evolution of the Unix time-sharing system. AT&T Bell Laboratories Technical Journal, 63(8), 1577–1593. https://doi.org/10.1002/j.1538-7305.1984.tb00054.x ↩︎

  6. Accetta, M., Baron, R., Bolosky, W., Golub, D., Rashid, R., Tevanian, A., & Young, M. (1986). Mach: A new kernel foundation for UNIX development. Proceedings of the USENIX Summer Conference, 93–112. ↩︎

  7. Custer, H. (1993). Inside Windows NT. Microsoft Press. ↩︎

  8. Microsoft. (n.d.). About processes and threads. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/procthread/about-processes-and-threads ↩︎

  9. Eykholt, J. R., Kleiman, S. R., Barton, S., Faulkner, R., Shivalingiah, A., Smith, M., Stein, D., Voll, J., Weeks, M., & Williams, D. (1992). Beyond multiprocessing: Multithreading the SunOS kernel. Proceedings of the USENIX Summer Conference, 11–18. ↩︎

  10. Oracle. (n.d.). Multithreading terms. In Multithreaded Programming Guide (Solaris 10). https://docs.oracle.com/cd/E19253-01/816-5137/6mba5vpj4/index.html ↩︎

  11. Anderson, T. E., Bershad, B. N., Lazowska, E. D., & Levy, H. M. (1992). Scheduler activations: Effective kernel support for the user-level management of parallelism. ACM Transactions on Computer Systems, 10(1), 53–79. https://doi.org/10.1145/146941.146944 ↩︎

  12. IEEE. (1995). IEEE Standard for Information Technology—Portable Operating System Interface (POSIX)—System Application Program Interface (API) Amendment 2: Threads Extension (C Language) (IEEE Std 1003.1c-1995). https://standards.ieee.org/ieee/1003.1c/1393/ ↩︎

  13. Linux man-pages project. (n.d.). clone(2). Linux manual page. https://man7.org/linux/man-pages/man2/clone.2.html ↩︎ ↩︎ ↩︎

  14. Linux man-pages project. (n.d.). pthreads(7). Linux manual page. https://man7.org/linux/man-pages/man7/pthreads.7.html ↩︎ ↩︎

  15. Drepper, U., & Molnar, I. (2005). The Native POSIX Thread Library for Linux. https://www.akkadia.org/drepper/nptl-design.pdf ↩︎

  16. Franke, H., Russell, R., & Kirkwood, M. (2002). Fuss, futexes and furwocks: Fast userlevel locking in Linux. Proceedings of the Ottawa Linux Symposium, 479–495. https://www.kernel.org/doc/ols/2002/ols2002-pages-479-495.pdf ↩︎ ↩︎

  17. Ousterhout, J. (1996, January 25). Why threads are a bad idea (for most purposes) [Invited talk]. USENIX Annual Technical Conference. https://web.stanford.edu/~ouster/cgi-bin/papers/threads.pdf ↩︎

  18. Kegel, D. (1999; last updated 2014). The C10K problem. http://www.kegel.com/c10k.html ↩︎

  19. nginx. (n.d.). Connection processing methods. https://nginx.org/en/docs/events.html ↩︎

  20. libuv. (n.d.). Design overview. libuv documentation. https://docs.libuv.org/en/v1.x/design.html ↩︎

  21. Lee, E. A. (2006). The problem with threads. Computer, 39(5), 33–42. https://doi.org/10.1109/MC.2006.180 ↩︎

  22. von Behren, R., Condit, J., & Brewer, E. (2003). Why events are a bad idea (for high-concurrency servers). Proceedings of the 9th Workshop on Hot Topics in Operating Systems. https://www.usenix.org/conference/hotos-ix/why-events-are-bad-idea-high-concurrency-servers ↩︎

  23. Java Community Process. (2004). JSR 133: Java Memory Model and Thread Specification Revision. https://jcp.org/en/jsr/detail?id=133 ↩︎ ↩︎

  24. Manson, J., Pugh, W., & Adve, S. V. (2005). The Java memory model. Proceedings of the 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, 378–391. https://doi.org/10.1145/1040305.1040336 ↩︎ ↩︎

  25. Boehm, H.-J., & Adve, S. V. (2008). Foundations of the C++ concurrency memory model. Proceedings of the 29th ACM SIGPLAN Conference on Programming Language Design and Implementation, 68–78. https://doi.org/10.1145/1375581.1375591 ↩︎ ↩︎

  26. Armstrong, J. (2007). A history of Erlang. Proceedings of the Third ACM SIGPLAN Conference on History of Programming Languages, 6-1–6-26. https://doi.org/10.1145/1238844.1238850 ↩︎

  27. The Go Authors. (n.d.). Effective Go: Goroutines. https://go.dev/doc/effective_go#goroutines ↩︎

  28. The Go Authors. (n.d.). Frequently asked questions: Why goroutines instead of threads? https://go.dev/doc/faq#goroutines ↩︎ ↩︎

  29. Pressler, R., & Bateman, A. (2023). JEP 444: Virtual threads. OpenJDK. https://openjdk.org/jeps/444 ↩︎ ↩︎

  30. procps-ng. (n.d.). ps(1): Process state codes. Linux manual page. https://man7.org/linux/man-pages/man1/ps.1.html ↩︎

  31. Linux man-pages project. (n.d.). fork(2). Linux manual page. https://man7.org/linux/man-pages/man2/fork.2.html ↩︎ ↩︎

  32. Linux man-pages project. (n.d.). PR_SET_CHILD_SUBREAPER(2const). Linux manual page. https://man7.org/linux/man-pages/man2/PR_SET_CHILD_SUBREAPER.2const.html ↩︎

  33. Microsoft. (n.d.). CreateProcessA function (processthreadsapi.h). Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa ↩︎

  34. Baumann, A., Appavoo, J., Krieger, O., & Roscoe, T. (2019). A fork() in the road. Proceedings of the Workshop on Hot Topics in Operating Systems, 14–22. https://doi.org/10.1145/3317550.3321435 ↩︎

  35. Sun Microsystems. (n.d.). Multithreading models. In Java on Solaris 7 Developer’s Guide, Chapter 2. https://docs.oracle.com/cd/E19620-01/805-4031/6j3qv1oej/index.html ↩︎

  36. Engelschall, R. S. (2003). GNU Pth: The GNU Portable Threads (README). https://gnu.cs.utah.edu/pth/README ↩︎

  37. Linux man-pages project. (n.d.). pthread_create(3). Linux manual page. https://man7.org/linux/man-pages/man3/pthread_create.3.html ↩︎

  38. Microsoft. (n.d.). Thread stack size. Microsoft Learn. https://learn.microsoft.com/en-us/windows/win32/procthread/thread-stack-size ↩︎

  39. Li, C., Ding, C., & Shen, K. (2007). Quantifying the cost of context switch. Proceedings of the 2007 Workshop on Experimental Computer Science, Article 2. https://doi.org/10.1145/1281700.1281702 ↩︎

  40. Amdahl, G. M. (1967). Validity of the single processor approach to achieving large scale computing capabilities. Proceedings of the April 18–20, 1967, Spring Joint Computer Conference, 483–485. https://doi.org/10.1145/1465482.1465560 ↩︎

  41. Hoare, C. A. R. (1974). Monitors: An operating system structuring concept. Communications of the ACM, 17(10), 549–557. https://doi.org/10.1145/355620.361161 ↩︎

  42. The Open Group. (2018). pthread_cond_timedwait, pthread_cond_wait. The Open Group Base Specifications Issue 7 (IEEE Std 1003.1-2017). https://pubs.opengroup.org/onlinepubs/9699919799/functions/pthread_cond_wait.html ↩︎

  43. Lamport, L. (1979). How to make a multiprocessor computer that correctly executes multiprocess programs. IEEE Transactions on Computers, C-28(9), 690–691. https://doi.org/10.1109/TC.1979.1675439 ↩︎

  44. Boehm, H.-J. (2005). Threads cannot be implemented as a library. Proceedings of the 2005 ACM SIGPLAN Conference on Programming Language Design and Implementation, 261–268. https://doi.org/10.1145/1065010.1065042 ↩︎

  45. Linux man-pages project. (n.d.). signal(7). Linux manual page. https://man7.org/linux/man-pages/man7/signal.7.html ↩︎

  46. nginx. (n.d.). Core functionality: worker_processes. https://nginx.org/en/docs/ngx_core_module.html#worker_processes ↩︎

  47. Reis, C., & Gribble, S. D. (2009). Isolating web programs in modern browser architectures. Proceedings of the 4th ACM European Conference on Computer Systems, 219–232. https://doi.org/10.1145/1519065.1519090 ↩︎

  48. The Chromium Authors. (n.d.). Process model and site isolation. Chromium Docs. https://chromium.googlesource.com/chromium/src/+/main/docs/process_model_and_site_isolation.md ↩︎

  49. The Chromium Projects. (n.d.). Site isolation. https://www.chromium.org/Home/chromium-security/site-isolation/ ↩︎