Threads

Threads are a fundamental part of modern computing and are often referred to as lightweight processes. Here's a breakdown of what threads are, why they are considered lightweight, what they share, and what they don't:

  1. What are Threads?

    • A thread is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. Threads are a way for a program to split itself into two or more simultaneously running tasks.
  2. Why are they called Lightweight Processes?

    • Threads are called lightweight processes because they have their own stack but can access shared data. Compared to processes, threads are less resource-intensive to create and destroy. While processes require a separate memory space, threads within the same process share the same memory space.
  3. What do Threads Share?

    • Heap Memory: Threads of the same process share the heap memory. This is the memory that is dynamically allocated during the execution of a program.

    • Global Variables: Since threads operate in the same memory space, they share the global variables.

    • File Descriptors and Code: They also share the code of the program they are part of and the file descriptors, which are the references to the files opened by the program.

  4. Why Don't They Share Certain Things?

    • Stack Memory: Each thread has its own stack memory. This is because the stack contains the thread's execution history and local variables, which are unique to each thread.

    • Registers and Program Counter: Each thread has its own registers and program counter to keep track of where it is in its instruction sequence.

Now, let's illustrate the memory layout of threads with an ASCII diagram:

+---------------------+
|     Process Memory  |
+---------------------+
|       Code          |  Shared by all threads
+---------------------+
|       Data          |  Shared by all threads
+---------------------+
|       Heap          |  Dynamically allocated shared memory
+---------------------+
| Thread 1 Stack      |  Unique to Thread 1
+---------------------+
| Thread 2 Stack      |  Unique to Thread 2
+---------------------+
|        ...          |
+---------------------+
| Thread N Stack      |  Unique to Thread N
+---------------------+

In this diagram:

  • The Code, Data, and Heap areas are shared among all threads of the process.

  • Each thread has its own Stack, which is not shared. This is where local variables, function calls, and return addresses are stored for each thread.

User-level and kernel-level threads are two fundamental types of threading mechanisms in computing systems. Understanding their differences is key to appreciating the various threading models like one-to-one, many-to-one, and many-to-many. Let's explore these concepts in detail:

User-Level Threads

  • User-level threads are managed entirely by the user without kernel support. They are implemented in user space, and the kernel is unaware of their existence.

  • Control: All thread management is done by the application through a thread library. Examples include POSIX Pthreads and Java threads.

  • Real-World Example: A Java application running multiple threads. The Java Virtual Machine manages these threads without the underlying operating system being aware of each individual thread.

  • Advantages:

    • Speed: Thread operations like creation, destruction, and switching are faster since they don't require kernel calls.

    • Portability: As they are managed in user space, they can run on any operating system.

  • Disadvantages:

    • Resource Utilization: If one thread blocks, such as waiting for I/O, all threads within the process can be blocked since the OS only sees a single process.

    • No Kernel-Level Privileges: User-level threads cannot take advantage of multiprocessing.

Kernel-Level Threads

  • Kernel-level threads are managed directly by the operating system kernel. The kernel is aware of and manages the scheduling of these threads.

  • Control: The operating system kernel is responsible for managing and scheduling kernel threads.

  • Real-World Example: Threads in Windows or Linux operating systems, where the OS kernel directly handles the threading.

  • Advantages:

    • Concurrency: Since the kernel controls the scheduling, different threads of the same process can run on different processors simultaneously.

    • Blocking: One thread can block for I/O without affecting the execution of other threads.

  • Disadvantages:

    • Slower: Operations like thread creation and context switching involve system calls, which are slower.

    • Complexity: More complex to implement, as they require kernel support.

Threading Models

One-to-One Model

  • Each user-level thread maps to one kernel thread.

  • Advantages: Provides more concurrency; blocking one thread doesn’t block others; can run on multiple processors.

  • Disadvantages: Creating a large number of user-level threads can be inefficient due to resource constraints, as each has a corresponding kernel thread.

  • Example: Modern Linux, Windows NT.

Many-to-One Model

  • Many user-level threads map to a single kernel thread.

  • Advantages: Thread operations are fast and efficient as they don't involve kernel mode privileges.

  • Disadvantages: A single blocking system call can block the entire process; cannot take advantage of multi-core processors as only one thread can access the kernel at a time.

  • Example: Older threading libraries like the original POSIX Pthreads.

Many-to-Many Model

  • Maps many user-level threads to many kernel threads.

  • Advantages: Combines the best aspects of one-to-one and many-to-one models; system can create as many kernel threads as needed to allow user-level threads to run in parallel on multiple processors.

  • Disadvantages: More complex to implement; requires coordination between user and kernel-level threads.

  • Example: IRIX, HP-UX, and the Solaris 9 and earlier versions.

Pthreads, short for POSIX threads, is a standard for multi-threading APIs (Application Programming Interfaces) designed to enable parallelism in software. It's part of the POSIX standard (Portable Operating System Interface), which is a family of standards specified by the IEEE for maintaining compatibility between operating systems. Pthreads are widely used in Unix-like operating systems such as Linux and macOS.

Common Pthread Functions

  1. pthread_create: Used to create a new thread.

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

    • This function starts a new thread in the calling process.

  2. pthread_join: Used to wait for a thread to terminate.

    • int pthread_join(pthread_t thread, void **retval);

    • This function blocks the calling thread until the specified thread terminates.

  3. pthread_exit: Exits the calling thread.

    • void pthread_exit(void *retval);

    • This function is used to terminate a thread and can return a value to another thread that's joining.

  4. pthread_mutex_lock and pthread_mutex_unlock: Used for locking and unlocking a mutex.

    • int pthread_mutex_lock(pthread_mutex_t *mutex);

    • int pthread_mutex_unlock(pthread_mutex_t *mutex);

    • These functions are used to synchronize threads by protecting a piece of code from being executed by more than one thread at a time.

Example C Program

Now, let's write a simple C program that creates 4 threads to add numbers in a distributed manner and reports the subtotals. The main thread then adds up these subtotals to find the final sum.

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

#define NUM_THREADS 4
#define ARRAY_SIZE 100

int array[ARRAY_SIZE];
int part = 0;
int sum = 0;
pthread_mutex_t sum_mutex;

void* sum_array(void* arg) {
    int thread_part = part++;
    int local_sum = 0;

    for (int i = thread_part * (ARRAY_SIZE / NUM_THREADS); i < (thread_part + 1) * (ARRAY_SIZE / NUM_THREADS); i++) {
        local_sum += array[i];
    }

    pthread_mutex_lock(&sum_mutex);
    sum += local_sum;
    pthread_mutex_unlock(&sum_mutex);

    printf("Subtotal by thread %d: %d\n", thread_part, local_sum);
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    pthread_mutex_init(&sum_mutex, NULL);

    // Initialize array with some values
    for (int i = 0; i < ARRAY_SIZE; i++) {
        array[i] = i + 1;
    }

    // Create threads
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_create(&threads[i], NULL, sum_array, NULL);
    }

    // Wait for threads to complete
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("Total sum: %d\n", sum);
    pthread_mutex_destroy(&sum_mutex);
    return 0;
}

Code Description

  • This program divides an array of 100 integers into 4 parts and assigns each part to a different thread to compute the subtotal.

  • Each thread calculates the sum of its portion of the array and adds this subtotal to the global sum variable.

  • A mutex (sum_mutex) is used to ensure that only one thread at a time can modify the global sum variable. This prevents data race conditions.

  • The main function waits for all threads to complete their computation using pthread_join and then prints the total sum.

  • Each thread prints its own subtotal before terminating.