Post

10. Threads

10. Threads

In the world of system programming, threads play a pivotal role in achieving concurrency and efficient resource utilization.

A thread is the smallest unit of execution within a process. Unlike processes, which have separate memory spaces, threads within the same process share:

  • Code
  • Data segment
  • Heap memory
  • Open files and sockets

However, each thread has its own:

  • Stack
  • Register set
  • Program counter (PC)

This shared memory model makes threads faster to create and manage compared to processes but requires careful synchronization to avoid race conditions.

Threading Models in Linux

In Linux, threads are implemented using the NPTL (Native POSIX Thread Library) (<pthread.h>), which uses the clone() system call under the hood. The two main threading models are:

  • User-Level Threads: Managed by user-level libraries without kernel awareness. They are lightweight but can block the entire process if one thread performs a blocking operation.
  • Kernel-Level Threads: Managed directly by the operating system kernel, allowing better scheduling and management. The kernel is aware of all threads and can schedule them independently.

Linux uses a 1:1 threading model, where each user thread maps directly to a kernel thread.

  • pthread is a standard set of thread management functions used to create, synchronize, and manage threads.
  • The pthread library provides functions for thread creation, synchronization, and termination, as well as for handling attributes like priority, stack size, and detachability.

Key Functions

  • pthread_create() – Creates a new thread.
  • pthread_join() – Waits for a thread to terminate.
  • pthread_exit() – Terminates the calling thread.
  • pthread_detach() – Allows a thread to run independently (no need for join).

Creating Threads in Linux

The pthread_create() function is used to create a new thread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <pthread.h>
#include <stdio.h>

void *thread_function(void *arg) {
    printf("Inside the thread!\n");
    return NULL;
}

int main() {
    pthread_t thread_id;
    int ret = pthread_create(&thread_id, NULL, thread_function, NULL);
    
    if (ret != 0) {
        perror("Thread creation failed");
        return 1;
    }
    
    pthread_join(thread_id, NULL); // Wait for the thread to finish
    printf("Thread execution complete.\n");
    return 0;
}

Thread Synchronization

In multithreaded applications, synchronization mechanisms are essential to manage access to shared resources. Common techniques include:

  • Mutexes: Used to ensure mutual exclusion when accessing shared data.
  • Condition Variables: Allow threads to wait for certain conditions to be met before proceeding.
  • Barriers: Synchronize multiple threads at specific points in execution, ensuring that all threads reach a certain point before any can proceed.

Single vs Multi-threaded Process

Refer:

linux thread

Info

When we talk about a process in traditional Unix/Linux terminology, we’re talking about:

An independent execution unit with:

  • Its own address space (virtual memory).
  • Own set of file descriptors.
  • One execution flow: A single program counter (PC), single stack, and registers.

Such a process naturally has only one thread of execution.
Thus, a “normal process” (without explicitly creating threads) is effectively a single-threaded process.

When a program starts:

  • It always begins with one thread of execution.
  • This initial thread is often called the main thread.

Difference Between exit() and pthread_exit()

Featureexit()pthread_exit()
PurposeTerminates the entire processTerminates calling thread only
Effect on Other ThreadsTerminates all threads immediatelyOther threads continue running normally
Resource CleanupCleans up process resources; runs atexit() handlersCleans up resources of the thread only
Return Value UseReturns status code to OS (process exit code)Thread return value (retrievable via pthread_join())
Typical Use CaseProcess termination (e.g., on fatal error)Thread termination in multi-threaded programs

Thread ID

Every thread—whether in a single-threaded or multi-threaded process—has a unique Thread ID (TID). It’s crucial for managing threads correctly.

A Thread ID (TID) uniquely identifies a thread within the system.

In Linux:

  • Every thread is represented by a kernel-level task (using clone() internally).
  • Each thread has:

    • A unique Thread ID (TID).
    • A Process ID (PID), often the same as TID in single-threaded programs.

Warning

pthread_t is not guaranteed to be an integer or TID—it’s an opaque handle in POSIX (though many systems internally use integers).

Following example will clear it -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

void* thread_func(void* arg) {
    printf("Thread: pthread_t = %lu, TID = %ld, PID = %d\n",
           pthread_self(), syscall(SYS_gettid), getpid());
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_func, NULL);

    printf("Main thread: pthread_t = %lu, TID = %ld, PID = %d\n",
           pthread_self(), syscall(SYS_gettid), getpid());

    pthread_join(thread, NULL);
    return 0;
}

1
2
3
$ ./tid 
Main thread: pthread_t = 140304230987584, TID = 295124, PID = 295124
Thread: pthread_t = 140304227235392, TID = 295125, PID = 295124

You’ll notice that the main thread’s TID is the same as the process’s PID. This is because the main thread represents the initial thread of the process, and it shares the same ID as the process itself. In short, it’s the initial thread that starts when the process begins.

Race Condition

When working with threads, one of the most common and dangerous problems is a race condition. A race condition occurs when multiple threads access shared data simultaneously, leading to unpredictable and incorrect behavior. I’ll what race conditions are, why they happen, and how to prevent them.

A race condition happens when:

  • Two or more threads access shared data at the same time.
  • At least one thread modifies the data.
  • The final result depends on the timing of thread execution.

Since the OS scheduler can interrupt threads at any point, the order of execution is non-deterministic, leading to inconsistent results.

F&F Race GIF

Example of a Race Condition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <pthread.h>
#include <stdio.h>

int counter = 0;

void *increment_counter(void *arg) {
    for (int i = 0; i < 100000; i++) {
        counter++; // Non-atomic operation (read-modify-write)
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, increment_counter, NULL);
    pthread_create(&thread2, NULL, increment_counter, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Final counter value: %d\n", counter); // Expected: 200000, but often less!
    return 0;
}

On compiling and running -

1
2
3
4
5
6
$ tmp  ./race 
Final counter value: 136530
$ tmp  ./race 
Final counter value: 166144
$ tmp  ./race 
Final counter value: 107189

Why does this fail?

  • counter++ is not atomic (it involves read → modify → write).
  • If two threads read counter at the same time, they may overwrite each other’s updates.
This post is licensed under CC BY 4.0 by the author.