Lesson 44
Multithreading (Pthreads)
Multithreading is the technique of running multiple threads within a single process. A thread is the smallest unit of execution in a program. In C programming on Unix-like systems, multithreading is implemented using the POSIX Threads (Pthreads) library. Threads share the same memory space, making communication fast but requiring careful synchronization using mutexes and condition variables to prevent race conditions.
C Track
Save progress as you learn.
44 lessons
Lesson content
Multithreading (Pthreads) in C Programming: A Complete Beginner to Advanced Guide
What is Multithreading in C Programming?
Modern computers have multiple CPU cores that can execute several tasks simultaneously. To take advantage of this hardware, applications often use multithreading. Instead of performing tasks one after another, a multithreaded program can execute multiple tasks concurrently, improving responsiveness and performance.
Multithreading is the technique of running multiple threads within a single process. A thread is the smallest unit of execution in a program. Unlike separate processes, threads share the same memory space, making communication between them fast and efficient. In C programming on Unix-like systems, multithreading is commonly implemented using the POSIX Threads (Pthreads) library.
Think of a process as a restaurant, and threads as the workers inside it. All workers share the same kitchen (memory), but each performs different tasks simultaneously.
Why Does Multithreading Exist?
Without multithreading, programs perform one task at a time, long-running operations can freeze applications, and multi-core processors are underutilized. With multithreading, multiple tasks execute concurrently, applications become more responsive, CPU resources are used more efficiently, and background processing becomes possible.
Real-World Use Cases
Multithreading is widely used in:
- Web servers
- Database systems
- Video and image processing
- Game engines
- File compression utilities
- Scientific computing
- Embedded systems
- Network servers
- Web browsers
- Operating systems
Prerequisites
Before learning Multithreading (Pthreads) in C programming, you should understand:
- Basic C programming
- Functions
- Pointers
- Structures
- Dynamic memory allocation
- Command-line compilation
- Basic understanding of processes
Core Concepts
What is a Thread?
A thread is an independent execution path within a process. Each thread has its own program counter, stack, and registers. Threads in the same process share global variables, heap memory, open files, and process resources.
Process vs Thread
| Feature | Process | Thread |
|---|---|---|
| Memory | Separate | Shared |
| Creation Cost | Higher | Lower |
| Communication | IPC required | Shared memory |
| Execution | Independent | Concurrent |
| Context Switch | Slower | Faster |
Pthreads Library
The POSIX Threads library provides functions for creating and managing threads. Include the header:
#include <pthread.h>| Function | Purpose |
|---|---|
| `pthread_create()` | Create a new thread |
| `pthread_join()` | Wait for a thread to finish |
| `pthread_exit()` | Terminate a thread |
| `pthread_self()` | Get current thread ID |
| `pthread_equal()` | Compare thread IDs |
Creating a Thread
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg
);Joining Threads
pthread_join(thread, NULL);Mutexes
A mutex (mutual exclusion) allows only one thread at a time to access a shared resource.
pthread_mutex_init()
pthread_mutex_lock()
pthread_mutex_unlock()
pthread_mutex_destroy()Condition Variables
Condition variables allow threads to wait until a specific condition becomes true.
pthread_cond_wait()
pthread_cond_signal()
pthread_cond_broadcast()Code Examples
Example 1: Beginner – Creating a Thread
#include <stdio.h> // Standard input/output library
#include <pthread.h> // POSIX Threads library
void *printMessage(void *arg) // Thread function
{
printf("Hello from the thread!\n"); // Display message
return NULL; // Exit thread
}
int main() // Program entry point
{
pthread_t thread; // Thread identifier
pthread_create(&thread, // Thread object
NULL, // Default attributes
printMessage, // Thread function
NULL); // No argument
pthread_join(thread, NULL); // Wait for thread to finish
return 0; // Exit successfully
}Example 2: Intermediate – Passing Arguments to a Thread
#include <stdio.h> // Standard I/O library
#include <pthread.h> // POSIX Threads library
void *square(void *arg) // Thread function
{
int number = *(int *)arg; // Retrieve integer argument
printf("Square = %d\n", number * number); // Compute square
return NULL; // Exit thread
}
int main() // Program entry point
{
pthread_t thread; // Thread identifier
int value = 5; // Integer to pass
pthread_create(&thread, // Create thread
NULL, // Default attributes
square, // Thread function
&value); // Pass address of value
pthread_join(thread, NULL); // Wait for completion
return 0; // Exit program
}Example 3: Intermediate – Race Condition
#include <stdio.h> // Standard I/O library
#include <pthread.h> // POSIX Threads library
int counter = 0; // Shared variable
void *increment(void *arg) // Thread function
{
for(int i = 0; i < 100000; i++) // Repeat many times
{
counter++; // Unsafe increment
}
return NULL; // Exit thread
}
int main() // Program entry point
{
pthread_t t1, t2; // Two thread identifiers
pthread_create(&t1, NULL, increment, NULL); // First thread
pthread_create(&t2, NULL, increment, NULL); // Second thread
pthread_join(t1, NULL); // Wait for first thread
pthread_join(t2, NULL); // Wait for second thread
printf("%d\n", counter); // Result is unpredictable
return 0; // Exit program
}Example 4: Advanced – Using a Mutex
#include <stdio.h> // Standard I/O library
#include <pthread.h> // POSIX Threads library
int counter = 0; // Shared variable
pthread_mutex_t lock; // Mutex object
void *increment(void *arg) // Thread function
{
for(int i = 0; i < 100000; i++) // Repeat many times
{
pthread_mutex_lock(&lock); // Lock mutex
counter++; // Safe increment
pthread_mutex_unlock(&lock);// Unlock mutex
}
return NULL; // Exit thread
}
int main() // Program entry point
{
pthread_t t1, t2; // Thread identifiers
pthread_mutex_init(&lock, NULL);// Initialize mutex
pthread_create(&t1, NULL, increment, NULL); // First thread
pthread_create(&t2, NULL, increment, NULL); // Second thread
pthread_join(t1, NULL); // Wait for first thread
pthread_join(t2, NULL); // Wait for second thread
printf("%d\n", counter); // Correct result
pthread_mutex_destroy(&lock); // Destroy mutex
return 0; // Exit program
}Example 5: Advanced – Thread Returning a Value
#include <stdio.h> // Standard I/O library
#include <stdlib.h> // Memory allocation
#include <pthread.h> // POSIX Threads library
void *compute(void *arg) // Thread function
{
int *result = malloc(sizeof(int)); // Allocate memory
*result = 100; // Store result
return result; // Return pointer
}
int main() // Program entry point
{
pthread_t thread; // Thread identifier
int *value; // Pointer for returned value
pthread_create(&thread, NULL, compute, NULL); // Create thread
pthread_join(thread, (void **)&value); // Retrieve result
printf("%d\n", *value); // Display result
free(value); // Free allocated memory
return 0; // Exit program
}Common Mistakes and Pitfalls
| Wrong | Correct |
|---|---|
| Forgetting pthread_join() | Wait for threads to finish |
| Ignoring return values of Pthread functions | Check for errors |
| Accessing shared data without synchronization | Use mutexes or other synchronization primitives |
| Locking a mutex but never unlocking it | Always unlock in every execution path |
| Destroying a mutex while still in use | Destroy only after all threads have finished |
Wrong:
counter++;Correct:
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);Best Practices
- Compile with -pthread and enable compiler warnings.
- Keep thread functions focused on a single task.
- Minimize the amount of code executed while holding a mutex.
- Protect every shared resource with appropriate synchronization.
- Avoid global variables unless necessary.
- Check the return values of all Pthread functions.
- Use condition variables instead of busy waiting.
- Prevent deadlocks by acquiring locks in a consistent order.
- Clean up mutexes, condition variables, and other synchronization objects.
- Test multithreaded programs under different workloads to uncover race conditions.
When NOT to Use This
Multithreading may not be the best solution when:
- Your application performs only simple sequential tasks.
- Thread creation overhead outweighs the performance benefit.
- The workload is primarily I/O-bound and asynchronous I/O is more appropriate.
- Shared data requires excessive synchronization, reducing concurrency.
- Simpler process-based parallelism or event-driven programming better fits the problem.
Summary / Key Takeaways
- Multithreading allows multiple threads to execute concurrently within the same process.
- Threads share memory, making communication fast but requiring careful synchronization.
- The Pthreads library provides APIs for creating, managing, and synchronizing threads.
- pthread_create() starts a new thread, while pthread_join() waits for it to finish.
- Race conditions occur when multiple threads access shared data without proper synchronization.
- Mutexes protect shared resources and prevent data corruption.
- Condition variables allow threads to wait efficiently for specific events.
- Proper synchronization, error checking, and cleanup are essential for reliable multithreaded applications.
FAQ About Multithreading (Pthreads) in C
1. What is multithreading in C programming?
Multithreading is the execution of multiple threads within the same process, allowing several tasks to run concurrently while sharing the process's resources.
2. What is the Pthreads library?
Pthreads (POSIX Threads) is the standard threading library for Unix-like operating systems. It provides APIs for creating, managing, and synchronizing threads.
3. What is a race condition?
A race condition occurs when two or more threads access and modify shared data simultaneously without proper synchronization, leading to unpredictable results.
4. What is the purpose of a mutex?
A mutex (mutual exclusion) ensures that only one thread at a time can access a shared resource, preventing data corruption caused by concurrent access.
5. Why should I use pthread_join()?
pthread_join() waits for a thread to complete its execution. It ensures that resources are released correctly and that the main thread does not exit before worker threads finish.