Creating POSIX Threads in Linux-Free Linux Device Drivers Course In Hyderabad

PREV_LEC | NEXT_LEC

Creating POSIX Threads in Linux
Lecture 7 of the free embedded Linux development course — pthread_create, thread IDs, and clean shutdown

A process can run more than one path of execution at once. Each of those paths is a thread, and on Linux they’re created and managed through the POSIX threads API, universally called pthreads. Unlike shared memory, which is for cooperating between separate processes, threads exist inside a single process and share its entire address space by default — every global variable, every heap allocation, every open file descriptor is visible to all of them. That makes threads cheaper to start and communicate through than processes, but it also means a bug in one thread can corrupt data another thread depends on. This lecture in our free linux device drivers course covers how threads are created, how the kernel actually sees them, and the different ways they can end.

pthread_create POSIX threads thread ID vs process ID pthread_join Light Weight Process free linux kernel development course free embedded linux course

What You Will Learn

  • What a thread actually is from the kernel’s point of view
  • Creating a thread with pthread_create() and passing it arguments
  • How to see your threads from outside the program with ps
  • The difference between a process ID and a thread ID on Linux
  • Every way a thread can terminate, and why unjoined threads leak resources
  • A complete, buildable multi-threaded demo using current glibc

Prerequisites

  • Comfortable with C function pointers and basic process concepts (PID, fork)
  • A Linux machine with gcc and glibc — pthreads ships as part of glibc on any current distro

What Is a Thread, Really?

On Linux, the kernel doesn’t have a deeply separate concept of “process” and “thread” the way some textbooks suggest. Both are created with the same underlying clone() system call; the only real difference is which resources are shared with the parent. A regular fork()ed child gets its own copy of the address space, file descriptor table, and so on. A pthread, by contrast, is cloned with almost everything — address space, open files, signal handlers — shared with the thread that created it. Internally, the kernel still schedules each thread as its own independent unit of execution, historically called a Light Weight Process (LWP).

One Process, Multiple Threads, Shared Address Space
Process (PID 4210) +—————————————————-+ | shared: heap, globals, open file descriptors | | | | Thread 1 (main, TID 4210) Thread 2 (TID 4211) | | own stack, own registers own stack, own registers| +—————————————————-+

Creating a Thread with pthread_create()

int pthread_create(pthread_t *thread,
                    const pthread_attr_t *attr,
                    void *(*start_routine)(void *),
                    void *arg);

It starts a new thread running start_routine, storing an opaque handle in *thread. Passing NULL for attr gives the new thread default scheduling behavior inherited from the caller; a non-NULL attr lets you override stack size, scheduling policy, or detached state before the thread starts. The new thread begins running essentially immediately — there’s no guarantee it runs before or after the calling thread continues past the pthread_create() call.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

struct ep_worker_args {
    int worker_id;
    int seconds_to_run;
};

static void *ep_worker_thread(void *arg)
{
    struct ep_worker_args *a = arg;

    printf("worker %d started, PID=%d TID=%d\n",
           a->worker_id, getpid(), gettid());

    sleep(a->seconds_to_run);

    printf("worker %d finishing\n", a->worker_id);
    return NULL;
}

int main(void)
{
    pthread_t workers[3];
    struct ep_worker_args args[3];

    printf("main thread, PID=%d TID=%d\n", getpid(), gettid());

    for (int i = 0; i < 3; i++) {
        args[i].worker_id      = i;
        args[i].seconds_to_run = 2 + i;

        if (pthread_create(&workers[i], NULL, ep_worker_thread, &args[i]) != 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    for (int i = 0; i < 3; i++)
        pthread_join(workers[i], NULL);

    printf("all workers finished\n");
    return 0;
}

Notice gettid() is called directly, with no syscall(SYS_gettid) workaround — that’s a real modernization worth knowing. Older glibc versions had no wrapper for the gettid syscall at all, forcing code to call it through syscall() by number. Since glibc 2.30, gettid() is a first-class libc function declared in <unistd.h>, so current code should just call it directly.

Thread IDs vs Process IDs

Every thread in a process shares the same PID, but each has its own unique TID, visible from outside the program using ps -eLf (the -L flag shows threads, listed under the LWP column):

$ ./ep_thread_demo &
$ ps -eLf | grep ep_thread_demo
UID       PID   PPID    LWP  NLWP STIME TTY  TIME CMD
ravi     8341   6210   8341     4 10:14 pts/0 00:00:00 ./ep_thread_demo
ravi     8341   6210   8342     4 10:14 pts/0 00:00:00 ./ep_thread_demo
ravi     8341   6210   8343     4 10:14 pts/0 00:00:00 ./ep_thread_demo
ravi     8341   6210   8344     4 10:14 pts/0 00:00:00 ./ep_thread_demo

All four rows share PID 8341 and PPID 6210 — they’re all part of the same process. The LWP column is the actual thread ID; the main thread’s LWP always matches the process’s PID, while every additional thread gets its own, higher LWP value. Some Linux-specific APIs will accept a TID where documentation says “PID” — that’s a Linux quirk, not portable behavior, and shouldn’t be relied on in code meant to run on other POSIX systems.

ColumnMeaning
PIDProcess ID — same for every thread in the process
PPIDParent process ID — same for every thread in the process
LWP / TIDThread ID — unique per thread; equals PID for the main thread only
NLWPTotal number of threads currently in the process

How Many Threads Can a Process Create?

There’s a system-wide ceiling on the total number of threads the kernel will schedule, readable at runtime:

$ cat /proc/sys/kernel/threads-max
62234

This scales with available RAM and typically ranges from around a thousand on small embedded targets to tens of thousands on larger systems. Once the limit is reached, both fork() and pthread_create() start failing (with EAGAIN) until existing threads or processes exit.

How a Thread Terminates

A thread ends in exactly one of four ways:

  • It reaches the end of its start routine and returns normally
  • It calls pthread_exit() explicitly, from anywhere in its call stack
  • Another thread cancels it with pthread_cancel()
  • The whole process terminates — a call to exit(), or an unhandled/unmasked signal

fork() and threads don’t mix cleanly

If a multi-threaded process calls fork(), only the calling thread exists in the child — the other threads simply vanish from the child’s point of view, without their exit handlers or cleanup ever running. Mutexes they were holding stay locked forever in the child. Mixing fork() with threads safely requires real care (pthread_atfork() handlers, or avoiding the combination entirely); it’s not something to reach for casually.

Why pthread_join() Matters

A thread’s return value is a void *, and another thread can collect it with pthread_join() — used for all three workers in the demo above. Until some thread joins with it, a finished thread’s resources (most notably its stack) are not released back to the process, exactly analogous to a zombie process waiting for its parent to call wait(). A long-running program that keeps creating threads and never joining them will slowly leak memory even though every individual thread finishes cleanly. If you genuinely never intend to join a thread, create it detached instead, using pthread_attr_setdetachstate() — that tells the kernel to release its resources automatically on exit.

Building and Running the Demo

$ gcc -O2 -Wall -o ep_thread_demo ep_thread_demo.c -pthread
$ ./ep_thread_demo
main thread, PID=8341 TID=8341
worker 0 started, PID=8341 TID=8342
worker 1 started, PID=8341 TID=8343
worker 2 started, PID=8341 TID=8344
worker 0 finishing
worker 1 finishing
worker 2 finishing
all workers finished

Note the -pthread flag rather than -lpthread — on current glibc, -pthread is the recommended way to link threaded programs; it sets the right compiler defines as well as linking the library, whereas bare -lpthread only does the linking step.

Common Mistakes and Troubleshooting

Never joining a thread: leads to a slow resource leak in long-running programs — see the pthread_join section above.
Passing a stack variable’s address into a thread that outlives it: if the parent’s loop variable is reused or goes out of scope before the new thread reads it, the thread sees garbage. The demo above avoids this by giving each worker its own dedicated struct in an array.
Assuming threads run in creation order: the scheduler decides actual execution order; don’t rely on worker 0 finishing before worker 1 unless you explicitly synchronize them.

Best Practices

  • Always check pthread_create()’s return value — unlike most libc calls it returns an error code directly, not via errno
  • Join every thread you create, or explicitly detach it — never leave the choice implicit
  • Give each thread its own private data rather than sharing loop variables by address
  • Avoid combining fork() with a multi-threaded process unless you fully understand the caveats above

Summary

Threads are independent, kernel-scheduled paths of execution that share their parent process’s address space and file descriptors, created with pthread_create() and identified individually by a thread ID (LWP) distinct from the shared process ID. A thread ends by finishing its routine, calling pthread_exit(), being canceled, or its whole process terminating — and pthread_join() is what actually reclaims a finished thread’s resources, so joining (or explicitly detaching) every thread you create isn’t optional if you want a leak-free program.

FAQ

What’s the real difference between a process and a thread on Linux?

Both are created with the same underlying clone() system call; a thread simply shares far more with its creator — address space, file descriptors, signal handlers — while a forked process gets its own private copies of those resources.

Do I need -lpthread or -pthread to compile threaded programs?

Use -pthread. It links the threading support and sets the correct compiler defines in one flag; -lpthread alone only handles linking.

Why does gettid() work without syscall() on modern systems?

Because glibc 2.30 added a direct gettid() wrapper in unistd.h. Older code that predates this had to invoke the gettid syscall manually via syscall(SYS_gettid).

What happens to other threads if one thread calls exit()?

exit() terminates the entire process, which ends every thread in it immediately — use pthread_exit() instead if you only want the calling thread to stop.

Is it safe to call fork() from a multi-threaded program?

Only with real care. The child process retains just the thread that called fork(); other threads’ locks and in-progress work simply disappear from the child’s perspective, which can leave mutexes permanently locked.

What is a detached thread?

A thread created with its detach state set so the kernel reclaims its resources automatically on exit, meaning no other thread needs to (or can) call pthread_join() on it.

How do I find the maximum number of threads my system allows?

Read /proc/sys/kernel/threads-max — it scales with available RAM and applies system-wide, not per process.

Continue the Free Embedded Linux Course

Next up: thread synchronization primitives — mutexes and condition variables in practice.

Next Lecture Course Index

PREV_LEC | NEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *