QUESTION
When a thread function returns, do all thread resources disappear immediately?
pthread_create prepares a libc thread descriptor plus stack/TLS mappings, then uses clone to create a kernel task that shares the same mm, files, and other resources. When the start routine returns, kernel execution ends, but a joinable thread's return value and some userspace metadata remain until pthread_join.
pthread_t is not the same API as a numeric TID. Code that reuses a handle after join/detach or performs operations other than comparison depends on implementation details.
STRUCTURE
Structure diagram
Every thread shares heap and global mappings, but each has its own stack, TLS, registers, and kernel task.
CALL PATH
Call path
The kernel task terminates at a different time from when the pthread library can reuse the stack and descriptor. Assign exactly one join owner to every joinable thread.
This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.
SOURCE COORDINATES
Linux 6.18.37 LTS source locations
Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.
| File | Function / structure | What to inspect |
|---|---|---|
| kernel/fork.c | copy_process(), copy_mm(), copy_files() | Use CLONE flags to select process/thread resource sharing |
| kernel/exit.c | do_exit() | Thread-task termination and clear_child_tid handling |
| kernel/futex/core.c | futex_wake() | The TID-clear wakeup used by the join implementation |
COMPLETE PROGRAM
Complete runnable example
The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.
cc -std=c17 -Wall -Wextra -O2 -pthread thread_join.c -o thread_join01#include <pthread.h>
02#include <stdint.h>
03#include <stdio.h>
04#include <stdlib.h>
05
06struct job {
07 int begin;
08 int end;
09};
10
11static _Thread_local int jobs_done;
12
13static void *worker(void *argument)
14{
15 const struct job *job = argument;
16 long *sum = malloc(sizeof(*sum));
17 if (sum == NULL)
18 return NULL;
19 *sum = 0;
20 for (int value = job->begin; value < job->end; ++value)
21 *sum += value;
22 jobs_done++;
23 return sum;
24}
25
26int main(void)
27{
28 struct job job = { .begin = 1, .end = 1000 };
29 pthread_t thread;
30 if (pthread_create(&thread, NULL, worker, &job) != 0)
31 return 1;
32 void *result;
33 if (pthread_join(thread, &result) != 0 || result == NULL)
34 return 1;
35 printf("sum=%ld main_tls=%d\n", *(long *)result, jobs_done);
36 free(result);
37 return 0;
38}
CODE NOTES
Code notes
static _Thread_local int jobs_doneEven with the same symbol name, each thread has a separate instance. Incrementing the worker's instance does not affect the main thread's instance.
const struct job *jobPasses the address of an object on the main stack. In this example it is safe because the main stack and job lifetime continue until pthread_join.
long *sum = mallocTransfers ownership of a heap object that remains valid after join instead of returning the address of a worker-stack local.
pthread_create(&threadReturns a pthread_t handle on success. pthread functions commonly return an error number directly rather than -1/errno.
pthread_join(threadSynchronizes with thread completion, receives the return value, and reclaims library resources for the joinable thread. The same thread must not be joined twice.
DETAILS
Detailed behavior
A thread stack is a fixed-size mapping
The default pthread stack size comes from process resource limits and implementation settings. A large local array or deep recursion can reach the stack guard page independently of the process-wide heap.
When changing it with pthread_attr_setstacksize, measure PTHREAD_STACK_MIN, alignment, and actual frame use.
detach declares background ownership
A detached thread is reclaimed automatically at exit and cannot be joined. Freeing context supplied by the caller immediately after detaching can cause use-after-free.
For a worker whose completion must be awaited during shutdown, a joinable state and explicit join owner are simpler.
Distinguish process exit from thread exit
When a worker returns or calls pthread_exit, only that thread ends. If any thread calls exit(), exit_group terminates the whole process.
Returning from main is also equivalent to exit, so use pthread_exit in main or a join structure if other threads must continue running.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
pthread descriptor | Created by pthread_create and reused/released after join or detached exit | pthread_t, result, cancel state |
thread stack/TLS | Mapped/allocated at creation and reclaimed after thread-exit cleanup | guard, stack size, TLS dtors |
task_struct | Created by clone and released by thread do_exit/release | TID, clear_child_tid, robust list |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| Thread count keeps increasing | Joinable threads are not joined | Inspect /proc/PID/task and stack mappings |
| The worker return value crashes | An address on the worker stack was returned | Inspect allocation ownership and lifetime |
| pthread_create EAGAIN | A task limit or insufficient stack VA/memory | ulimit -u, pids cgroup, maps |
LAB
Verify it yourself
- Remove join, repeatedly create threads, and observe Threads in /proc/PID/status and memory usage.
- Print the worker stack base/size with pthread_attr_getstack and match it against /proc/PID/maps.
- Print the address of a _Thread_local variable in several threads to confirm separate TLS instances.
./thread_joinstrace -f -e trace=clone3,clone,futex,set_robust_list,exit ./thread_joinPRIMARY REFERENCES