CoreTechED

Loading

Learn C multithreading with pthreads: thread creation, joining, mutex synchronization, and common issues. Master concurrent programming.

Multithreading in C: Complete Guide with Examples

Multithreading in C: A Comprehensive Tutorial

Multithreading is a technique in which a process is divided into multiple threads that execute concurrently. It allows a program to perform multiple tasks simultaneously within a single process, improving performance and responsiveness. Threads are lightweight execution units within a process that share resources while executing independently.

This tutorial provides an in-depth exploration of multithreading in C using POSIX Threads (pthreads): thread creation, management, synchronization, common issues, and best practices.


Conceptual Introduction & Fundamentals

What is a Thread?

A thread is an independent sequence of execution within a process. Unlike processes, threads share the same memory space and operating system resources.

Key characteristics:

  • Each thread has its own program counter, registers, and stack.
  • Threads share code, data, files, and signals.
  • Thread creation is faster than process creation.
  • Multiple threads can execute concurrently.

Single-Threaded vs. Multithreaded Process

AspectSingle-ThreadedMultithreaded
ExecutionOne task at a timeMultiple tasks concurrently
ResponsivenessMay block on I/OOther threads continue
Resource usageLower overheadHigher CPU utilization
ComplexitySimplerMore complex (synchronization)
PerformanceLimitedBetter for parallel tasks

Why Multithreading Matters

  • Performance: Utilize multiple CPU cores.
  • Responsiveness: Keep UI responsive while processing.
  • Resource sharing: Efficient use of memory and files.
  • Parallelism: Execute independent tasks simultaneously.
  • Scalability: Handle multiple concurrent requests.

POSIX Threads (pthreads)

The POSIX Threads library provides APIs for creating, managing, synchronizing, and terminating threads in C.

Header: <pthread.h>

Compilation: gcc program.c -lpthread


Syntax & Core Mechanics

Creating a Thread

Syntax:

c

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

Parameters:

  • thread: Stores the ID of the newly created thread
  • attr: Thread attributes (NULL for default)
  • start_routine: Function executed by the thread
  • arg: Argument passed to the thread function

Example:

c

#include <pthread.h> #include <stdio.h> #include <stdlib.h> void *threadFunction(void *arg) { printf("Thread is running\n"); return NULL; } int main(void) { pthread_t thread; if (pthread_create(&thread, NULL, threadFunction, NULL) != 0) { perror("pthread_create"); return EXIT_FAILURE; } pthread_join(thread, NULL); return EXIT_SUCCESS; }

Expected output:

text

Thread is running

Waiting for a Thread to Finish

pthread_join blocks the calling thread until the specified thread terminates.

Syntax:

c

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

Example:

c

#include <pthread.h> #include <stdio.h> void *threadFunction(void *arg) { printf("Thread is running.\n"); return NULL; } int main(void) { pthread_t thread; pthread_create(&thread, NULL, threadFunction, NULL); /* Wait for thread to finish */ pthread_join(thread, NULL); printf("Thread finished.\n"); return 0; }

Expected output:

text

Thread is running. Thread finished.

Terminating a Thread

pthread_exit terminates the calling thread explicitly.

Syntax:

c

void pthread_exit(void *retval);

Cancelling a Thread

pthread_cancel requests cancellation of a thread.

Syntax:

c

int pthread_cancel(pthread_t thread);

Getting Current Thread ID

pthread_self returns the calling thread’s ID.

Syntax:

c

pthread_t pthread_self(void);

Passing Arguments to Threads

c

#include <pthread.h> #include <stdio.h> #include <stdlib.h> void *printNumber(void *arg) { int *num = (int *)arg; printf("Number: %d\n", *num); return NULL; } int main(void) { pthread_t thread; int value = 42; pthread_create(&thread, NULL, printNumber, &value); pthread_join(thread, NULL); return 0; }

Expected output:

text

Number: 42

Thread Synchronization with Mutex

Mutexes ensure only one thread accesses a resource at a time.

c

#include <pthread.h> #include <stdio.h> #include <stdlib.h> int sharedCounter = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void *incrementCounter(void *arg) { for (int i = 0; i < 1000000; i++) { pthread_mutex_lock(&mutex); sharedCounter++; pthread_mutex_unlock(&mutex); } return NULL; } int main(void) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, incrementCounter, NULL); pthread_create(&thread2, NULL, incrementCounter, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Final counter: %d\n", sharedCounter); pthread_mutex_destroy(&mutex); return 0; }

Expected output:

text

Final counter: 2000000

Full Practical Code Example with Explanation

This program demonstrates multithreading in a realistic scenario: a concurrent task processor with synchronization.

Program: Multithreaded Task Processor

c

/** * multithreading_demo.c * Demonstrates pthreads with thread creation, synchronization, * and a realistic task processing scenario. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> #include <unistd.h> #include <errno.h> #define NUM_THREADS 4 #define NUM_TASKS 20 #define MAX_LINE_LEN 100 /* Shared data structure */ typedef struct { int tasks[NUM_TASKS]; int taskIndex; pthread_mutex_t mutex; int totalProcessed; } TaskQueue; /* Thread argument structure */ typedef struct { int threadId; TaskQueue *queue; } ThreadArg; /* Function prototypes */ void initializeQueue(TaskQueue *queue); int getNextTask(TaskQueue *queue); void processTask(int threadId, int task); void *workerThread(void *arg); void demonstrateBasicThreads(void); void demonstrateSynchronization(void); void demonstrateTaskProcessor(void); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Multithreading Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Basic Thread Creation\n"); printf("2. Thread Synchronization (Mutex)\n"); printf("3. Task Processor (Multiple Threads)\n"); printf("4. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: demonstrateBasicThreads(); break; case 2: demonstrateSynchronization(); break; case 3: demonstrateTaskProcessor(); break; case 4: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Initializes the task queue */ void initializeQueue(TaskQueue *queue) { for (int i = 0; i < NUM_TASKS; i++) { queue->tasks[i] = i * 10; } queue->taskIndex = 0; queue->totalProcessed = 0; pthread_mutex_init(&queue->mutex, NULL); } /* Gets the next task from the queue (thread-safe) */ int getNextTask(TaskQueue *queue) { int task = -1; pthread_mutex_lock(&queue->mutex); if (queue->taskIndex < NUM_TASKS) { task = queue->tasks[queue->taskIndex]; queue->taskIndex++; } pthread_mutex_unlock(&queue->mutex); return task; } /* Processes a task */ void processTask(int threadId, int task) { printf("Thread %d processing task: %d\n", threadId, task); usleep(100000); // Simulate work (100ms) } /* Worker thread function */ void *workerThread(void *arg) { ThreadArg *threadArg = (ThreadArg *)arg; int threadId = threadArg->threadId; TaskQueue *queue = threadArg->queue; printf("Thread %d started.\n", threadId); while (1) { int task = getNextTask(queue); if (task == -1) { break; // No more tasks } processTask(threadId, task); pthread_mutex_lock(&queue->mutex); queue->totalProcessed++; pthread_mutex_unlock(&queue->mutex); } printf("Thread %d finished.\n", threadId); return NULL; } /* Demonstrates basic thread creation */ void demonstrateBasicThreads(void) { printf("\n=== Basic Thread Creation ===\n"); pthread_t thread1, thread2; pthread_create(&thread1, NULL, (void *(*)(void *))workerThread, NULL); pthread_create(&thread2, NULL, (void *(*)(void *))workerThread, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Both threads completed.\n"); } /* Demonstrates mutex synchronization */ void demonstrateSynchronization(void) { printf("\n=== Thread Synchronization (Mutex) ===\n"); int counter = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /* Simulate multiple threads incrementing counter */ printf("Using mutex to safely increment counter...\n"); /* In this demo, we use a simple loop without actual threads */ for (int i = 0; i < 100; i++) { pthread_mutex_lock(&mutex); counter++; pthread_mutex_unlock(&mutex); } printf("Counter after mutex-protected increments: %d\n", counter); pthread_mutex_destroy(&mutex); } /* Demonstrates task processor with multiple threads */ void demonstrateTaskProcessor(void) { printf("\n=== Task Processor (Multiple Threads) ===\n"); TaskQueue queue; initializeQueue(&queue); pthread_t threads[NUM_THREADS]; ThreadArg threadArgs[NUM_THREADS]; /* Create worker threads */ for (int i = 0; i < NUM_THREADS; i++) { threadArgs[i].threadId = i + 1; threadArgs[i].queue = &queue; pthread_create(&threads[i], NULL, workerThread, &threadArgs[i]); } /* Wait for all threads to finish */ for (int i = 0; i < NUM_THREADS; i++) { pthread_join(threads[i], NULL); } printf("\nAll tasks processed: %d\n", queue.totalProcessed); pthread_mutex_destroy(&queue.mutex); }

Explanation of Multithreading Usage

  1. Thread creation: pthread_create with function and arguments.
  2. Thread joining: pthread_join waits for completion.
  3. Mutex: Protects shared counter and task queue.
  4. Task queue: Thread-safe task distribution.
  5. Thread arguments: Struct passed to thread function.
  6. Concurrent processing: Multiple threads process tasks simultaneously.

Compilation

bash

gcc -Wall -Wextra -std=c11 multithreading_demo.c -lpthread -o multithreading ./multithreading

Expected Output (partial)

text

=== Multithreading Demo === --- Menu --- 1. Basic Thread Creation 2. Thread Synchronization (Mutex) 3. Task Processor (Multiple Threads) 4. Exit Enter choice: 3 === Task Processor (Multiple Threads) === Thread 1 started. Thread 2 started. Thread 3 started. Thread 4 started. Thread 1 processing task: 0 Thread 2 processing task: 10 Thread 3 processing task: 20 Thread 4 processing task: 30 Thread 1 processing task: 40 Thread 2 processing task: 50 ... Thread 4 finished. Thread 1 finished. Thread 2 finished. Thread 3 finished. All tasks processed: 20

Common Pitfalls, Edge Cases & Memory Considerations

1. Race Conditions

Multiple threads accessing shared data without synchronization.

c

int counter = 0; // Shared without protection void *increment(void *arg) { counter++; // BUG: race condition }

Solution: Use mutexes or atomic operations.

2. Deadlocks

Two threads waiting for resources held by each other.

c

pthread_mutex_lock(&mutex1); pthread_mutex_lock(&mutex2); // Thread A // Meanwhile Thread B locks mutex2 then mutex1

Solution: Use consistent locking order; avoid nested locks when possible.

3. Forgetting pthread_join

Not joining threads can cause resource leaks or premature termination.

c

pthread_create(&thread, NULL, func, NULL); /* Missing pthread_join */

Solution: Always join or detach threads.

4. Passing Stack Variables to Threads

Passing pointers to local variables that go out of scope.

c

void *threadFunc(void *arg) { int *value = (int *)arg; printf("%d\n", *value); // BUG: value may be destroyed } void createThread(void) { int local = 42; pthread_create(&thread, NULL, threadFunc, &local); // BUG }

Solution: Use heap allocation or ensure variable stays alive.

5. Not Checking Return Values

Thread functions can fail; always check return values.

c

pthread_create(&thread, NULL, func, NULL); // No check

Solution: Check if (pthread_create(...) != 0).

6. Data Races on Shared Variables

Unsynchronized access to shared variables causes undefined behavior.

Solution: Use mutexes, atomics, or thread-local storage.

7. Over-Synchronization

Using mutexes excessively reduces parallelism and performance.

Solution: Minimize critical sections; use read-write locks when possible.

8. Thread Starvation

Low-priority threads may never get CPU time.

Solution: Use fair scheduling; avoid busy-wait loops.

9. Memory Leaks in Threads

Threads that allocate memory and don’t free it cause leaks.

Solution: Free allocated memory in the thread before exit.

10. Thread-Safe Functions

Some functions (like strtok, localtime) are not thread-safe.

Solution: Use thread-safe variants (strtok_r, localtime_r).


Summary & Best Practices

Multithreading enables concurrent execution within a process, improving performance and responsiveness. Proper synchronization is essential to avoid race conditions and deadlocks.

Key Takeaways

  • Threads share memory and resources.
  • Use pthreads library for multithreading in C.
  • pthread_create: Create threads.
  • pthread_join: Wait for threads.
  • pthread_mutex_lock/unlock: Synchronize access.
  • Compile with -lpthread.
  • Common issues: race conditions, deadlocks, starvation.

Best Practices

  1. Always check pthread function return values.
  2. Use mutexes for shared resource protection.
  3. Keep critical sections short to minimize contention.
  4. Always join or detach threads.
  5. Pass heap-allocated or persistent data to threads.
  6. Use thread-safe functions in multi-threaded code.
  7. Avoid global variables where possible.
  8. Document thread safety of functions.
  9. Test with different thread counts.
  10. Use tools like Valgrind/Helgrind to detect race conditions.

By mastering multithreading, you’ll write efficient concurrent programs that utilize system resources effectively while avoiding common concurrency pitfalls.