Lesson 14-C Language Multithreading

Thread Basics

Differences Between Threads and Processes

  • Process: The basic unit of resource allocation (independent memory space, file handles, etc.).
  • Thread: The basic unit of CPU scheduling (shares process memory, lightweight).
  • Thread States: Created, ready, running, blocked, terminated.
  • Thread ID: Each thread has a unique identifier.

Advantages:

  • Shared memory: Thread communication does not require complex IPC (such as pipes or shared memory).
  • Low overhead: Creating/switching threads is more efficient than processes.

Introduction to POSIX Threads (pthread)

pthread is the POSIX standard thread library, supported on Linux, macOS, and other systems (Windows requires adaptation via MinGW or Cygwin). Core functions include:

  • pthread_create: Create a thread.
  • pthread_join: Wait for a thread to terminate.
  • pthread_mutex_*: Mutex operations.
  • pthread_cond_*: Condition variable operations.

Thread Creation and Basic Operations

Thread Creation: pthread_create

pthread_create is used to create a new thread, with the following prototype:

#include <pthread.h>

int pthread_create(
    pthread_t *thread,       // Thread ID (output parameter)
    const pthread_attr_t *attr,  // Thread attributes (NULL for default)
    void *(*start_routine)(void *),  // Thread function (entry point)
    void *arg                  // Argument passed to the thread function
);

Example: Creating a Simple Thread

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>  // sleep()

// Thread function: print thread ID and argument
void* thread_func(void *arg) {
    int thread_id = *(int*)arg;
    printf("Thread %d started\n", thread_id);
    sleep(1);  // Simulate work
    printf("Thread %d ended\n", thread_id);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    int arg1 = 1, arg2 = 2;

    // Create thread 1
    if (pthread_create(&tid1, NULL, thread_func, &arg1) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // Create thread 2
    if (pthread_create(&tid2, NULL, thread_func, &arg2) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // Wait for threads to finish
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("Main thread ended\n");
    return 0;
}

Compilation Command (must link pthread library):

gcc -o thread_demo thread_demo.c -lpthread

Output:

Thread 1 started
Thread 2 started
Thread 1 ended
Thread 2 ended
Main thread ended

Thread Synchronization Mechanisms

When multiple threads concurrently access shared resources, synchronization mechanisms are needed to avoid race conditions. Common synchronization mechanisms include mutexes, condition variables, and semaphores.

Thread Synchronization

  • Mutex (pthread_mutex_t): Used to protect shared resources.
  • Condition Variable (pthread_cond_t): Used for inter-thread communication.

Thread Synchronization

Below is an example using a mutex and condition variable to demonstrate how to synchronize access between threads.

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

int count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *incrementer(void *arg) {
    int id = *(int *)arg;
    while (1) {
        pthread_mutex_lock(&mutex);

        if (count >= 10) {
            pthread_cond_wait(&cond, &mutex);
        } else {
            count++;
            printf("Thread %d incremented count to %d\n", id, count);
            pthread_cond_signal(&cond);
        }

        pthread_mutex_unlock(&mutex);
        usleep(100000); // Sleep 100 milliseconds
    }

    free(arg);
    pthread_exit(NULL);
}

int main() {
    pthread_t thread1, thread2;
    int *id1 = malloc(sizeof(int));
    int *id2 = malloc(sizeof(int));

    *id1 = 1;
    *id2 = 2;

    // Create threads
    if (pthread_create(&thread1, NULL, incrementer, id1) != 0) {
        perror("pthread_create failed");
        return 1;
    }
    if (pthread_create(&thread2, NULL, incrementer, id2) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // Wait for threads to finish
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Cleanup
    free(id1);
    free(id2);

    return 0;
}

Mutex

A mutex is used to protect a critical section, ensuring only one thread accesses a shared resource at a time.

Mutex Operation Functions

FunctionDescription
pthread_mutex_initInitialize mutex
pthread_mutex_lockLock (blocks until lock is acquired)
pthread_mutex_unlockUnlock
pthread_mutex_destroyDestroy mutex

Example: Mutex Protecting a Shared Variable

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

#define THREAD_NUM 5

int shared_counter = 0;          // Shared counter
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  // Static mutex initialization

void* increment_counter(void *arg) {
    for (int i = 0; i < 1000; i++) {
        pthread_mutex_lock(&mutex);  // Lock
        shared_counter++;            // Critical section
        pthread_mutex_unlock(&mutex); // Unlock
    }
    return NULL;
}

int main() {
    pthread_t tids[THREAD_NUM];

    // Create 5 threads
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_create(&tids[i], NULL, increment_counter, NULL);
    }

    // Wait for all threads to finish
    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_join(tids[i], NULL);
    }

    printf("Final counter value: %d (expected %d)\n", shared_counter, THREAD_NUM * 1000);
    return 0;
}

Output (when no race condition):

Final counter value: 5000 (expected 5000)

Condition Variable

Condition variables are used for inter-thread communication, allowing threads to wait until a specific condition is met. They are typically used with a mutex.

Condition Variable Operation Functions

FunctionDescription
pthread_cond_initInitialize condition variable
pthread_cond_waitWait for condition (automatically releases lock, reacquires on wake)
pthread_cond_signalWake one waiting thread
pthread_cond_broadcastWake all waiting threads
pthread_cond_destroyDestroy condition variable

Example: Producer-Consumer Model (Simplified)

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

#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int in = 0, out = 0;  // Produce/consume positions

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;   // Buffer not full condition
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;  // Buffer not empty condition

// Producer thread: add data to buffer
void* producer(void *arg) {
    int item;
    for (int i = 0; i < 10; i++) {
        item = i;  // Produce data (example: 0–9)
        pthread_mutex_lock(&mutex);
        // Wait for buffer not full
        while (in == BUFFER_SIZE) {
            pthread_cond_wait(¬_full, &mutex);
        }
        buffer[in++] = item;
        printf("Producer %ld produced: %d (buffer position %d)\n", (long)pthread_self(), item, in-1);
        pthread_cond_signal(¬_empty);  // Wake consumer
        pthread_mutex_unlock(&mutex);
        usleep(100000);  // Simulate production time
    }
    return NULL;
}

// Consumer thread: remove data from buffer
void* consumer(void *arg) {
    int item;
    for (int i = 0; i < 10; i++) {
        pthread_mutex_lock(&mutex);
        // Wait for buffer not empty
        while (out == in) {
            pthread_cond_wait(¬_empty, &mutex);
        }
        item = buffer[out++];
        printf("Consumer %ld consumed: %d (buffer position %d)\n", (long)pthread_self(), item, out-1);
        pthread_cond_signal(¬_full);  // Wake producer
        pthread_mutex_unlock(&mutex);
        usleep(200000);  // Simulate consumption time
    }
    return NULL;
}

int main() {
    pthread_t prod_tid, cons_tid;

    // Create producer and consumer threads
    pthread_create(&prod_tid, NULL, producer, NULL);
    pthread_create(&cons_tid, NULL, consumer, NULL);

    // Wait for threads to finish
    pthread_join(prod_tid, NULL);
    pthread_join(cons_tid, NULL);

    return 0;
}

Key Logic:

  • Producer waits for not_full condition (buffer not full) before producing.
  • Consumer waits for not_empty condition (buffer not empty) before consuming.
  • After producing/consuming, use pthread_cond_signal to wake the other thread.

Thread Management and Advanced Features

Thread Management

  • pthread_join: Wait for a thread to terminate.
  • pthread_detach: Detach a thread so it does not need to be joined.

Example Code

Below is a simple multithreading example that demonstrates how to create two threads and have them print messages.

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

// Thread function
void *print_message(void *arg) {
    int id = *(int *)arg;
    printf("Hello from thread %d\n", id);
    free(arg); // Free passed argument
    pthread_exit(NULL);
}

int main() {
    pthread_t thread1, thread2;
    int *id1 = malloc(sizeof(int));
    int *id2 = malloc(sizeof(int));

    *id1 = 1;
    *id2 = 2;

    // Create threads
    if (pthread_create(&thread1, NULL, print_message, id1) != 0) {
        perror("pthread_create failed");
        return 1;
    }
    if (pthread_create(&thread2, NULL, print_message, id2) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // Wait for threads to finish
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Cleanup
    free(id1);
    free(id2);

    return 0;
}

Thread Detachment

By default, a thread must be joined using pthread_join after termination. A detached thread automatically releases resources upon termination and does not require joining.

Detached Thread Example

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

void* detached_thread(void *arg) {
    printf("Detached thread started\n");
    sleep(1);
    printf("Detached thread ended (resources auto-released)\n");
    return NULL;
}

int main() {
    pthread_t tid;

    // Create detached thread (set attribute to PTHREAD_CREATE_DETACHED)
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    if (pthread_create(&tid, &attr, detached_thread, NULL) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // No need to join detached thread
    sleep(2);  // Main thread waits enough time
    printf("Main thread ended\n");
    return 0;
}

Thread Cancellation

Use pthread_cancel to terminate a thread. It requires cancellation points (e.g., sleep, read) or setting cancellation state.

Thread Cancellation Example

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

void* cancellable_thread(void *arg) {
    printf("Cancellable thread started\n");
    while (1) {
        printf("Running...\n");
        sleep(1);  // Cancellation point (sleep is a system call, allows cancellation)
    }
    return NULL;
}

int main() {
    pthread_t tid;

    if (pthread_create(&tid, NULL, cancellable_thread, NULL) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    sleep(3);  // Main thread waits 3 seconds
    printf("Attempting to cancel thread\n");
    pthread_cancel(tid);  // Send cancellation request

    // Wait for thread to finish (optional)
    void *retval;
    pthread_join(tid, &retval);
    if (retval == PTHREAD_CANCELED) {
        printf("Thread was canceled\n");
    }

    return 0;
}

Thread Attributes (pthread_attr_t)

Thread attributes are used to set stack size, scheduling policy, priority, etc. Common attributes include:

  • detachstate: Detach state (PTHREAD_CREATE_JOINABLE or PTHREAD_CREATE_DETACHED).
  • stacksize: Thread stack size (default usually 8MB).

Setting Thread Stack Size

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

void* large_stack_thread(void *arg) {
    char large_array[1024 * 1024];  // 1MB array (test stack size)
    printf("Thread stack size test completed\n");
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_attr_t attr;
    size_t stack_size = 2 * 1024 * 1024;  // Set stack size to 2MB

    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, stack_size);  // Set stack size

    if (pthread_create(&tid, &attr, large_stack_thread, NULL) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    pthread_join(tid, NULL);
    pthread_attr_destroy(&attr);
    return 0;
}

Thread-Local Storage (TLS)

TLS allows each thread to have its own copy of a variable, suitable for thread-private data (e.g., thread ID, error code).

Using __thread Keyword (GCC Extension)

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

__thread int thread_local_var = 0;  //

void* tls_thread(void *arg) {
    int tid = *(int*)arg;
    thread_local_var = tid * 10;  // Modify own copy
    printf("Thread %d's TLS variable value: %d\n", tid, thread_local_var);
    return NULL;
}

int main() {
    pthread_t tids[2];
    int args[2] = {1, 2};

    pthread_create(&tids[0], NULL, tls_thread, &args[0]);
    pthread_create(&tids[1], NULL, tls_thread, &args[1]);

    pthread_join(tids[0], NULL);
    pthread_join(tids[1], NULL);

    return 0;
}

Output:

Thread 1's TLS variable value: 10
Thread 2's TLS variable value: 20

Thread Scheduling

Thread scheduling determines execution order. Priority and scheduling policy can be controlled via thread attributes.

Example: Setting Thread Scheduling

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

void *scheduled_thread(void *arg) {
    int id = *(int *)arg;
    int i = 0;

    while (1) {
        printf("Thread %d is running, iteration %d\n", id, i++);
        usleep(100000); // Sleep 100 milliseconds
    }

    free(arg);
    pthread_exit(NULL);
}

int main() {
    pthread_t thread1, thread2;
    pthread_attr_t attr;
    int *id1 = malloc(sizeof(int));
    int *id2 = malloc(sizeof(int));

    *id1 = 1;
    *id2 = 2;

    // Initialize thread attributes
    if (pthread_attr_init(&attr) != 0) {
        perror("pthread_attr_init failed");
        return 1;
    }

    // Set scheduling policy
    struct sched_param param;
    param.sched_priority = 50; // Set priority
    if (pthread_attr_setschedpolicy(&attr, SCHED_FIFO) != 0) {
        perror("pthread_attr_setschedpolicy failed");
        return 1;
    }
    if (pthread_attr_setschedparam(&attr, ¶m) != 0) {
        perror("pthread_attr_setschedparam failed");
        return 1;
    }

    // Create threads
    if (pthread_create(&thread1, &attr, scheduled_thread, id1) != 0) {
        perror("pthread_create failed");
        return 1;
    }
    if (pthread_create(&thread2, &attr, scheduled_thread, id2) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // Destroy thread attributes
    if (pthread_attr_destroy(&attr) != 0) {
        perror("pthread_attr_destroy failed");
        return 1;
    }

    // Main thread continues
    printf("Main thread continues...\n");

    // Cleanup
    free(id1);
    free(id2);

    return 0;
}

Thread Stack

The thread stack is the space used to store local variables and function call information. Stack size can be specified via thread attributes.

Example: Setting Thread Stack Size

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

void *stack_thread(void *arg) {
    int id = *(int *)arg;
    int i = 0;

    while (1) {
        printf("Thread %d is running, iteration %d\n", id, i++);
        usleep(100000); // Sleep 100 milliseconds
    }

    free(arg);
    pthread_exit(NULL);
}

int main() {
    pthread_t thread1, thread2;
    pthread_attr_t attr;
    int *id1 = malloc(sizeof(int));
    int *id2 = malloc(sizeof(int));

    *id1 = 1;
    *id2 = 2;

    // Initialize thread attributes
    if (pthread_attr_init(&attr) != 0) {
        perror("pthread_attr_init failed");
        return 1;
    }

    // Set thread stack size
    size_t stack_size = 2 * 1024 * 1024; // 2MB
    if (pthread_attr_setstacksize(&attr, stack_size) != 0) {
        perror("pthread_attr_setstacksize failed");
        return 1;
    }

    // Create threads
    if (pthread_create(&thread1, &attr, stack_thread, id1) != 0) {
        perror("pthread_create failed");
        return 1;
    }
    if (pthread_create(&thread2, &attr, stack_thread, id2) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // Destroy thread attributes
    if (pthread_attr_destroy(&attr) != 0) {
        perror("pthread_attr_destroy failed");
        return 1;
    }

    // Main thread continues
    printf("Main thread continues...\n");

    // Cleanup
    free(id1);
    free(id2);

    return 0;
}

Complete Producer-Consumer Model Example

The producer-consumer model is a classic multithreading synchronization problem that coordinates producer and consumer speeds to avoid buffer overflow or idle waiting. Below is a complete implementation:

Model Design

  • Buffer: Fixed-size circular queue (avoids false overflow).
  • Mutex: Protects buffer read/write operations.
  • Condition Variables: not_full (buffer not full, producer can produce) and not_empty (buffer not empty, consumer can consume).

Complete Code

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

#define BUFFER_SIZE 5
#define PRODUCER_NUM 2
#define CONSUMER_NUM 2
#define ITEM_NUM 10  // Each producer produces 10 items

typedef struct {
    int *buffer;     // Buffer array
    int in;          // Producer write position
    int out;         // Consumer read position
    int count;       // Current number of items in buffer (optional, for optimization)
    pthread_mutex_t mutex;
    pthread_cond_t not_full;
    pthread_cond_t not_empty;
} Buffer;

// Initialize buffer
void buffer_init(Buffer *buf) {
    buf->buffer = (int*)malloc(BUFFER_SIZE * sizeof(int));
    buf->in = 0;
    buf->out = 0;
    buf->count = 0;
    pthread_mutex_init(&buf->mutex, NULL);
    pthread_cond_init(&buf->not_full, NULL);
    pthread_cond_init(&buf->not_empty, NULL);
}

// Destroy buffer
void buffer_destroy(Buffer *buf) {
    free(buf->buffer);
    pthread_mutex_destroy(&buf->mutex);
    pthread_cond_destroy(&buf->not_full);
    pthread_cond_destroy(&buf->not_empty);
}

// Producer thread function
void* producer(void *arg) {
    Buffer *buf = (Buffer*)arg;
    for (int i = 0; i < ITEM_NUM; i++) {
        int item = rand() % 100;  // Generate random item (0–99)

        pthread_mutex_lock(&buf->mutex);
        // Wait for buffer not full
        while (buf->count == BUFFER_SIZE) {
            printf("Producer %ld waiting for buffer (current %d/%d)\n", 
                   (long)pthread_self(), buf->count, BUFFER_SIZE);
            pthread_cond_wait(&buf->not_full, &buf->mutex);
        }

        // Produce item
        buf->buffer[buf->in] = item;
        buf->in = (buf->in + 1) % BUFFER_SIZE;
        buf->count++;
        printf("Producer %ld produced: %d (buffer %d/%d)\n", 
               (long)pthread_self(), item, buf->count, BUFFER_SIZE);

        pthread_cond_signal(&buf->not_empty);  // Wake consumer
        pthread_mutex_unlock(&buf->mutex);

        usleep(rand() % 200000);  // Random production time (0–200ms)
    }
    return NULL;
}

// Consumer thread function
void* consumer(void *arg) {
    Buffer *buf = (Buffer*)arg;
    for (int i = 0; i < ITEM_NUM; i++) {
        pthread_mutex_lock(&buf->mutex);
        // Wait for buffer not empty
        while (buf->count == 0) {
            printf("Consumer %ld waiting for buffer (current %d/%d)\n", 
                   (long)pthread_self(), buf->count, BUFFER_SIZE);
            pthread_cond_wait(&buf->not_empty, &buf->mutex);
        }

        // Consume item
        int item = buf->buffer[buf->out];
        buf->out = (buf->out + 1) % BUFFER_SIZE;
        buf->count--;
        printf("Consumer %ld consumed: %d (buffer %d/%d)\n", 
               (long)pthread_self(), item, buf->count, BUFFER_SIZE);

        pthread_cond_signal(&buf->not_full);  // Wake producer
        pthread_mutex_unlock(&buf->mutex);

        usleep(rand() % 300000);  // Random consumption time (0–300ms)
    }
    return NULL;
}

int main() {
    Buffer buf;
    buffer_init(&buf);

    pthread_t prod_tids[PRODUCER_NUM];
    pthread_t cons_tids[CONSUMER_NUM];

    // Create producer threads
    for (int i = 0; i < PRODUCER_NUM; i++) {
        pthread_create(&prod_tids[i], NULL, producer, &buf);
    }

    // Create consumer threads
    for (int i = 0; i < CONSUMER_NUM; i++) {
        pthread_create(&cons_tids[i], NULL, consumer, &buf);
    }

    // Wait for all threads to finish
    for (int i = 0; i < PRODUCER_NUM; i++) {
        pthread_join(prod_tids[i], NULL);
    }
    for (int i = 0; i < CONSUMER_NUM; i++) {
        pthread_join(cons_tids[i], NULL);
    }

    buffer_destroy(&buf);
    return 0;
}

Compile and Run:

gcc -o producer_consumer producer_consumer.c -lpthread
./producer_consumer

Output Explanation:

  • Producers/consumers coordinate via condition variables to avoid buffer overflow or idle waiting.
  • Use while loops to check conditions (not if) to prevent spurious wakeups.

Key Summary

  • Thread Creation: Use pthread_create, pay attention to parameter passing and error checking.
  • Synchronization Mechanisms: Mutex protects critical sections, condition variables coordinate inter-thread dependencies.
  • Thread Management: Detached threads reduce resource usage, thread cancellation requires careful resource cleanup.
  • Advanced Features: TLS for thread-private data, stack size settings prevent overflow.
Share your love