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
| Aspect | Single-Threaded | Multithreaded |
|---|---|---|
| Execution | One task at a time | Multiple tasks concurrently |
| Responsiveness | May block on I/O | Other threads continue |
| Resource usage | Lower overhead | Higher CPU utilization |
| Complexity | Simpler | More complex (synchronization) |
| Performance | Limited | Better 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 threadattr: Thread attributes (NULL for default)start_routine: Function executed by the threadarg: 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
- Thread creation:
pthread_createwith function and arguments. - Thread joining:
pthread_joinwaits for completion. - Mutex: Protects shared counter and task queue.
- Task queue: Thread-safe task distribution.
- Thread arguments: Struct passed to thread function.
- 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
- Always check pthread function return values.
- Use mutexes for shared resource protection.
- Keep critical sections short to minimize contention.
- Always join or detach threads.
- Pass heap-allocated or persistent data to threads.
- Use thread-safe functions in multi-threaded code.
- Avoid global variables where possible.
- Document thread safety of functions.
- Test with different thread counts.
- 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.



