Calling a function creates another stack frame in the current thread. Creating a thread does something fundamentally different: it establishes a new execution context that the kernel can schedule independently.
For a POSIX application on Linux, this operation usually begins with:
The new thread eventually calls:
That compact interface hides work in both the user-space thread library and the kernel. A stack must be prepared, thread-local state must be initialized, a Linux task must be created, and the kernel must be given enough CPU context to begin the new instruction stream.
Teardown has a similar split. The thread stops executing in the kernel, but some user-space resources may need to remain until another thread collects its result.
A start function identifies the code a new thread should execute. It does not by itself provide an execution context.
The new thread also needs:
Conceptually:
start_routine and its argument.start_routine(argument) finally runs.The application supplies the start function and one pointer-sized argument. The library and kernel establish everything required to make that function an independently scheduled execution path.
The POSIX interface has this shape:
The parameters have distinct roles.
thread receives a pthread_t value that identifies the new thread to the POSIX library. This value is opaque: its representation is implementation-specific and it is not necessarily the Linux TID.
attributes controls properties established at creation. Passing NULL requests the default attributes.
start_routine is the function the new thread will run.
argument is passed unchanged to that function. The library copies the pointer value, not the object it points to.
On success, pthread_create returns 0. On failure, it returns an error number directly. Unlike many system calls, it does not report failure by returning -1 and setting errno.
Correct error handling therefore looks like:
Possible failures include insufficient resources for another thread, invalid attributes, or a request for a scheduling configuration the caller is not permitted to use.
If creation fails, no new thread exists and the output pthread_t value must not be used as though creation succeeded.
pthread_create is a library interface, not a thin request containing only a function address.
Before asking the kernel to create a task, a Linux POSIX thread implementation typically prepares several user-space objects.
The library obtains a stack region for the new thread. It also normally arranges a guard region near the stack: an inaccessible memory region intended to turn some stack overflows into a fault instead of silently letting the stack grow into adjacent mapped memory.
A guard region reduces risk but does not make every stack misuse detectable. A large jump past the guard or an arbitrary bad pointer can still reach other mapped memory.
The stack has a configured size. It must be large enough for the thread's deepest call chain, local variables, and architecture-specific needs. A thread that places large buffers on its stack or uses deep recursion may require more than the default assumptions of the application.
The library prepares the thread's thread-local-storage area and the architecture-specific register state needed to locate it.
This gives each thread its own instance of thread-local variables, including library-managed per-thread state such as errno.
The implementation maintains bookkeeping associated with the pthread_t handle. The record may track the thread's stack, return value, detach state, cancellation state, and information needed by pthread_join.
The exact layout is private to the thread library. Application code uses POSIX functions rather than modifying it directly.
The initial context is arranged so that the new task begins in a thread-library startup routine rather than jumping blindly into the application function.
This startup routine is often called a trampoline. It completes thread initialization, invokes start_routine(argument), captures the returned value, and routes normal return through the thread-exit path.
After the user-space preparation, the thread library invokes a Linux task-creation mechanism.
Linux uses clone-family operations for this purpose. The library requests a new task that shares the process's address space and process-wide resources while receiving its own execution context, TID, kernel stack, and scheduling state.
Conceptually, the kernel:
Real POSIX thread creation also gives the kernel addresses used to publish and later clear thread identity information. These details let the library coordinate startup and termination with the kernel.
Application code should normally use pthread_create, not construct a raw clone call. The library must correctly coordinate stack layout, thread-local storage, signal conventions, TID bookkeeping, and teardown across the C library and kernel.
Loading simulation...
After the kernel makes the new task runnable, either thread may run first:
The creating thread and the new thread are not ordered relative to each other after the call. start_routine may already be running before pthread_create returns, so code that assumes otherwise has a race.
There is no general guarantee that pthread_create returns to the creator before the new thread begins. On another CPU, the new thread may even finish before the creating thread returns from pthread_create.
This fact has immediate consequences:
A common error is to pass the address of one loop variable to several threads:
Every call passes the same address. The creator continues modifying index, and the loop may end before some workers read it. Workers can observe repeated or unexpected values, and they may access the object after its lifetime ends.
A safer structure gives each thread a stable argument object:
The array must remain alive until the workers have finished using it. Unique elements solve the aliasing problem; sufficient lifetime solves the dangling-pointer problem.
The kernel does not need to understand the C type of start_routine or POSIX return-value semantics. It starts the new task at an entry point chosen by the thread library.
The startup path is conceptually:
Once the kernel schedules the new task, the thread-library trampoline runs and:
start_routine(argument).void * value.Once the start routine is running, it is ordinary application code. It can call other functions, allocate memory, make system calls, and use process resources just like the initial thread.
The application does not call the trampoline directly. It is implementation machinery that converts the kernel's low-level task entry into the POSIX thread interface.
A thread can terminate normally in two equivalent ways.
It can return from its start routine:
Or it can call:
Returning from the start routine is treated as though the thread called pthread_exit with the returned pointer.
pthread_exit terminates only the calling thread. It does not directly terminate the other threads in the process.
This differs from process termination. Calling exit, calling _Exit, or returning from main terminates the process and therefore ends all of its threads. A fatal process-directed event can have the same process-wide result.
| Action | What ends |
|---|---|
| Return from a worker start routine | That worker thread |
pthread_exit(value) | The calling thread |
Return from main | The process, and therefore every thread |
exit(status) | The process, and therefore every thread |
The initial thread can call pthread_exit instead of returning from main if the design intentionally requires other threads to continue. Doing so keeps the process alive until its remaining threads finish. In most applications, explicit joining gives lifetime relationships that are easier to understand.
Normal thread termination is not one atomic deletion. User-space and kernel responsibilities occur in an ordered path.
The thread library performs work such as:
The thread then asks the kernel to end the calling Linux task. The kernel:
Ending one thread does not close the process's shared file descriptors or destroy the shared heap. Other threads still use those resources.
The same rule applies to ordinary heap allocations. Memory allocated by a thread does not automatically become “owned by the kernel” and freed when that thread exits. It remains allocated until application or library logic releases it, or until the entire process ends.
Automatic variables on the terminating thread's stack cease to be valid. A thread must never return a pointer to one of its local variables:
The joining thread would receive a pointer into a stack whose contents and lifetime are no longer valid.
POSIX threads are joinable by default.
When a joinable thread terminates, its kernel execution has stopped, but the implementation retains enough user-space state for another thread to collect its termination result and release associated resources.
Another thread performs that collection with:
If the target is still running, pthread_join waits. If the target has already terminated, the join can complete immediately.
Conceptually:
pthread_join delivers the result and reclaims the retained resources.Joining serves two purposes:
Only one successful join should collect a particular thread. Joining the same thread more than once is not a valid lifecycle strategy. A thread must also not attempt to join itself, because it cannot terminate while blocked waiting for its own termination.
POSIX threads are peers. The thread that created a target is not the only thread permitted to join it. Any suitable thread can do so, provided the program arranges exactly one responsible joiner and keeps the pthread_t value valid.
A terminated joinable thread is sometimes informally compared with a zombie process. The comparison is limited. Linux does not leave it as an ordinary waitable child process for waitpid; the thread library retains user-space resources and termination information until pthread_join.
Failing to join joinable threads that repeatedly terminate can therefore leak resources even though those threads no longer consume CPU time.
A detached thread does not retain a joinable termination result.
It can be created detached through thread attributes or detached later with:
When a detached thread terminates, the implementation can reclaim its thread-library resources without waiting for another thread to join it.
A detached thread runs, terminates, and has its resources reclaimed automatically.
A detached thread cannot later be joined. Detachment is therefore a lifecycle decision, not an instruction to run the thread in the background or at lower priority.
Detachment is appropriate only when no thread needs to collect the return value and the application has another reliable way to coordinate process shutdown.
It does not solve application-level resource ownership automatically. A detached thread must still release any heap objects, file-related state, or application resources for which it is responsible.
A detached thread may also still be running when main returns. Returning from main terminates the process; detached status does not allow the thread to survive process termination.
Every created POSIX thread should have a clear reclamation path:
Both end in the same place. The choice is who does the reclaiming, and a joinable thread that nobody joins is never reclaimed at all.
The invalid designs are:
| Mistake | Consequence |
|---|---|
| A joinable thread exits and nobody joins it | Retained resources accumulate |
Code calls pthread_join on a detached thread | No joinable result exists |
| Several threads race to join the same target | Lifecycle ownership is undefined or erroneous |
The choice should be made from ownership:
Neither form automatically makes shutdown correct. The process still needs a policy for whether outstanding work must finish before process termination.
It is tempting to imagine a safe kill_thread() operation that stops any thread at any instruction and cleans up everything it touched. General-purpose thread libraries cannot provide that guarantee.
A thread may be:
Asynchronous destruction at that point could leave the shared process in an inconsistent state.
POSIX cancellation is therefore a controlled protocol with configurable behavior and cleanup support. Ordinary designs usually prefer a cooperative request: the thread learns that shutdown is required, finishes or abandons work at a safe point, releases what it owns, and returns normally.
The kernel can always terminate the entire process as one failure boundary. Safely tearing down one thread while preserving every shared invariant requires cooperation from the application and its libraries.
This Linux program creates three threads with stable argument objects. Each worker writes its result into the object owned by main and returns the same pointer. The initial thread joins every worker and validates the returned pointer before using the result.
Compile and run it:
One possible output is:
The worker lines can appear in any order because the scheduler controls when each thread runs. The joined lines appear in array order because main calls pthread_join in that order.
The example also handles partial creation. If creating one thread fails, main still joins every thread that was created successfully. It does not discard their joinable resources.
The input array belongs to main and remains alive through every join, so worker argument pointers remain valid. Each thread uses a distinct array element, avoiding the shared-loop-variable error.
Default thread attributes are sufficient for many applications, but POSIX allows a program to configure properties before creation:
The fragment below focuses on the attribute sequence. Each function returns an error code that complete application code must check.
Destroying the attribute object after pthread_create does not destroy the thread. The library has already consumed the requested settings.
An application can also provide stack memory explicitly, but doing so transfers important responsibilities to the application. The memory must have valid size and alignment, remain allocated for the complete thread lifetime, and not be reused while the thread can access it.
When the library allocated the stack, it can normally reclaim or cache that storage after a detached thread exits or a joinable thread is joined. A library may cache recently used stacks for later thread creation, so process memory measurements do not have to fall immediately after a thread is reclaimed.
When the application supplied the stack, the thread library does not own that memory. The application must wait until the thread is no longer using it before releasing or reusing it.
Process-wide resources remain while at least one thread continues to use the process.
If one worker exits:
When thread A exits, threads B and C continue, and the shared address space and file-descriptor table remain in place.
When the final thread exits, there is no execution context left in the process. The kernel can complete process-wide termination and release the remaining process resources.
Process termination can also end all threads at once. Returning from main or calling exit does not wait for arbitrary joinable or detached workers to finish. The process termination path ends them.
This is why thread reclamation and process shutdown are related but separate concerns:
Creating a POSIX thread requires cooperation between the thread library and kernel. The library prepares the user stack, thread-local storage, control record, and startup trampoline; the kernel creates a schedulable task with its own TID, kernel stack, and scheduling state.
The new thread may run immediately, so arguments must be initialized and remain alive before creation. Returning from the start routine or calling pthread_exit ends only that thread, while returning from main or calling exit terminates the entire process.
A joinable thread retains termination information and resources until pthread_join collects them. A detached thread releases those resources automatically and cannot be joined. Every thread should have one deliberate reclamation path.
5 quizzes