A backend service may appear to be one running program, yet it can have several activities in progress:
The kernel cannot schedule “the service” as one indivisible object if these activities must run and wait independently. It needs a separate execution context for each one.
That execution context is a thread.
To the kernel, a thread is an independently schedulable execution context: the state required to run one sequence of instructions and resume it later.
This definition is more precise than saying that a thread is a “lightweight process.” The word lightweight describes a possible cost difference, but it does not explain what the object is. A thread is the unit that the scheduler can place on a logical CPU, preempt, block, wake, and later continue.
A CPU executes instructions according to state held in its registers. Two registers are especially important:
General-purpose registers hold operands, addresses, return values, and intermediate results. Status registers hold execution flags and other control state.
Together, this state describes one point in one computation:
| Thread A holds | Which points to |
|---|---|
| Instruction pointer | The next instruction in handle_request() |
| Stack pointer | The current call frame |
| Registers | Arguments and intermediate values |
Another thread can execute the same program code while being at a completely different point:
| Register | Thread A | Thread B |
|---|---|---|
| Instruction pointer | parse_request() | compress_response() |
| Stack pointer | A's call frames | B's call frames |
| General registers | A's values | B's values |
The code itself does not define a thread. Several threads can execute the same function, and one thread can execute many functions over its lifetime. What distinguishes them is their independent execution state.
A thread is also sequential from its own point of view. Its instruction pointer describes one current position. A single thread does not run on two CPUs at once. If an application wants two instruction streams to execute simultaneously on two logical CPUs, it needs at least two runnable threads or equivalent schedulable entities.
When the kernel chooses what should run on a CPU, it chooses a thread.
Suppose one process contains three threads:
Thread B cannot make progress until its data arrives, so it is not a useful scheduling candidate. The scheduler can choose A or C:
Thread B is not a candidate at all. The scheduler picks among threads that can actually make progress, so a waiting thread costs nothing but the memory that describes it.
If A blocks while reading from a socket, the kernel can run C. If the network data for B arrives, the kernel marks B runnable and considers it for CPU time again.
This is why statements such as “the process is running” are convenient but imprecise. A single-threaded process has only one thread, so the distinction is easy to ignore. In a multithreaded process, its threads can be in different states at the same moment:
There is no single scheduling state that accurately describes the whole process. Scheduling state belongs to each thread.
On a four-CPU machine, four runnable threads from one process could execute in parallel. The kernel may also place threads from four different processes on those CPUs. From the scheduler's perspective, both cases present independently schedulable work.
Only a running thread has its live execution state in a CPU's registers. When the thread stops running, the kernel must preserve enough information to continue it later as though the interruption had not happened.
The kernel therefore maintains several categories of per-thread state.
Every kernel-scheduled thread needs an identity so the kernel, debuggers, tracing tools, and system calls can refer to that specific execution context.
On Linux, this identity is the thread ID, or TID. Each thread has a distinct TID within its PID namespace.
The saved execution context includes the instruction pointer, stack pointer, general-purpose registers, status information, and architecture-specific state needed to resume execution.
Conceptually:
The exact save and restore mechanics are architecture-dependent. The important idea is that a suspended thread is not just “some code that will run again.” It has a precise continuation point.
The scheduler needs per-thread metadata such as:
CPU affinity applies naturally to threads. Two threads in the same process can be allowed to run on different sets of CPUs.
Each ordinary application thread follows its own chain of function calls, so it needs its own user-space stack and stack pointer.
Threads A and B execute the same shared program code, but each has its own user stack.
The word own describes how the stack is used, not necessarily a hardware-enforced protection boundary. Stacks for threads in the same process normally occupy different regions of the same virtual address space. A faulty pointer in one thread can therefore corrupt another thread's stack.
When a thread enters the kernel through a system call, exception, or interrupt, the kernel needs a safe stack on which to execute kernel code for that thread. Linux gives each task its own kernel stack.
This separation is necessary because several threads can be inside the kernel at once on different CPUs. A thread can also sleep while partway through a system call. Its kernel call chain must remain intact until that same thread wakes and continues.
The two threads never share a stack on either side of the boundary. Each has its own kernel stack, which is what lets two threads of the same process be inside the kernel at the same time.
The kernel stack is not the same object as the user stack. Application code cannot use it as ordinary memory, and the kernel switches to the appropriate kernel stack when handling that thread in kernel mode.
The kernel also tracks details such as a thread's signal mask, per-thread pending signals, thread-local-storage setup, and bookkeeping needed by tracing and debugging facilities.
Not every attribute associated with a running program is stored separately per thread. A thread also refers to larger resource objects associated with its process. That distinction lets the kernel preserve an independent execution path while several threads use a common application environment.
A useful mental model separates execution state from process resources.
Execution state answers:
Where is this instruction stream, and what does it need to continue?
Process resources answer:
What environment and kernel-managed objects can this instruction stream use?
The execution state includes each thread's registers, scheduling state, stack usage, and identity. The common resources include the program's address space and several kernel-managed resource collections.
For this lesson, the important point is the separation itself. A thread does not need an independent copy of every application resource in order to be scheduled independently. The exact sharing boundary between processes and threads is a separate design question.
This model also explains why the kernel can block one thread without automatically making every thread in the process unable to run. If Thread A waits for a socket operation, its execution state becomes waiting. Thread B has a different execution state and may remain runnable while using the same application environment.
Consider two request-handling threads in one server:
If no data is available for client A, the kernel may put Thread A to sleep. Thread A no longer competes for a CPU because it cannot complete the read yet.
Thread B can continue:
A's wait is not wasted CPU time. The moment A blocks, B becomes the thread the CPU runs.
When data arrives, the kernel records that Thread A is eligible to run again. It does not restart the thread from the beginning. A eventually resumes through the system-call path and continues from the instruction after the read call, with the call returning its result.
The general rule is:
A blocking operation changes the schedulability of the thread that performed it.
Some events deliberately affect an entire process, and application-level dependencies can make other threads unable to do useful work. Those cases do not change the kernel's basic scheduling unit: each thread still has its own execution and scheduling state.
Linux uses the word task as a unifying kernel concept for schedulable execution contexts. Each thread is represented by its own task descriptor, commonly discussed through the task_struct data structure.
This can be surprising because user-space terminology draws a strong line between processes and threads, while Linux represents both with tasks.
A Linux task owns:
A single-threaded process has one task. A multithreaded process has multiple tasks organized into a thread group.
The task descriptor does not contain every byte of every resource directly. It contains fields and references that connect the task to address-space information, open-file information, credentials, signal state, namespaces, and other kernel objects. Some referenced objects are common to a thread group, while other state belongs to one task.
The practical consequence is simple:
On Linux, adding an ordinary kernel-scheduled thread adds another task the scheduler can run.
It does not create another schedulable subunit inside one opaque process object. The new thread is itself visible to the scheduler.
Linux exposes two related identities that are often both displayed as a “PID.”
A TID identifies one task—that is, one kernel-scheduled thread.
A thread group ID, or TGID, identifies the collection of tasks that user space normally calls a process. The thread-group leader's TID becomes the TGID for the group.
Suppose a process has three threads:
The group takes its ID from the leader's TID, which is why the first thread's number appears in two places.
The initial thread has TID 8400, so its TID and TGID are equal. The other threads have their own TIDs, but all three report the same TGID.
In ordinary Linux user-space output, the TGID is usually labeled PID because it identifies the process as a whole. This gives the following relationship:
| Call | Returns | Across threads |
|---|---|---|
getpid() | The thread group ID | The same result in every thread |
gettid() | The calling thread's ID | Different for each thread |
This naming can make diagnostic output confusing. A monitoring tool may aggregate CPU use under the process PID, while a profiler or debugger reports the TID of the particular thread consuming the CPU.
A POSIX pthread_t value is a separate, library-level thread identifier. Programs should treat it as opaque. It is not a portable substitute for the Linux kernel TID.
Loading simulation...
Linux exposes the tasks in a process through:
The directory contains one subdirectory per TID. If process 8400 has three threads, its task directory might contain:
Each thread directory exposes information about that particular task. For example, its status file includes Tgid and Pid fields:
Here, Tgid identifies the process, while Pid identifies the individual Linux task. In this per-thread file, the field named Pid is effectively the TID.
The ps command can display the same distinction:
PID shows the common process identity, TID shows each thread identity, and PSR shows the logical CPU on which the thread most recently ran.
For an interactive per-thread view, Linux top supports:
This is useful when a process appears to consume one full CPU. The process-level number alone does not reveal whether one thread is saturated or many threads are sharing the work.
The following Linux program starts two additional threads only so that their kernel identities can be observed. Thread creation mechanics are not the focus here.
Save it as thread-identity.c, then compile and run it:
An example run may print:
The exact numbers change on every run. The relationship does not:
getpid() return the same thread-group identity.gettid() returns the identity of the calling task./proc/<PID>/task/ contains an entry for each live thread.During the sleep calls, the worker threads are waiting rather than consuming CPU time. They still exist as kernel tasks; they are simply not runnable until their timers expire.
A thread's lifetime begins when the kernel establishes a new execution context and makes it eligible to run. Its lifetime ends when that execution context exits and the kernel completes the required cleanup.
Threads in one process can begin and end at different times. A long-running server might create a set of worker threads during startup, retain them for hours, and shut them down when the service exits.
The kernel must distinguish:
This is another consequence of separating execution state from resources. Ending one instruction stream does not imply that every resource reachable by the remaining threads should disappear.
The user-space runtime or thread library also performs bookkeeping around thread startup and termination. The low-level creation and teardown protocol is more involved than merely allocating a stack, but those mechanics do not change the definition of the resulting kernel object: it is a new schedulable execution context.
A thread is the kernel's independently schedulable execution context. It represents one instruction stream and carries the identity, saved CPU state, stacks, scheduling metadata, and other information needed to stop and later resume that computation.
The scheduler operates on runnable threads, so threads in one process can be running, waiting, and waking independently. On Linux, each thread is represented as a task with its own TID, while tasks in the same process belong to one thread group identified by a common TGID, usually shown to user space as the PID.
The central mental model is:
A process provides an execution environment; a thread is one schedulable path of execution through that environment.
5 quizzes