Introduction
In today’s computing landscape, threads and multithreading are fundamental to building fast and efficient systems. From web servers to gaming engines, these concepts are essential for improving performance.
However, these advantages also introduce challenges such as deadlocks and data races.
In this article, we will explore:
- What threads are
- How multithreading works
- What deadlocks and data races are
- How to prevent common synchronization issues
- Practical C examples using POSIX threads (
pthread)
What Are Threads?
Threads are the smallest units of execution inside a program.
A single process can contain multiple threads running concurrently, allowing the program to perform several tasks simultaneously.
Using threads allows us to:
- Perform multiple operations at the same time
- Improve efficiency on multicore processors
- Increase responsiveness in applications
Thread Example
Here is a basic example of creating a thread using pthread.
#include <stdio.h>
#include <pthread.h>
void* hello_thread(void* arg)
{
printf("Hello from thread!\n");
return NULL;
}
int main()
{
pthread_t thread;
pthread_create(&thread, NULL, hello_thread, NULL);
pthread_join(thread, NULL);
return 0;
}
Explanation
pthread_create()creates a new threadpthread_join()waits for the thread to finish- Both threads run inside the same process
Compile with:
gcc main.c -pthread
What Is Multithreading?
Multithreading refers to running multiple threads concurrently inside a single process.
This allows programs to:
- Handle multiple tasks simultaneously
- Improve CPU utilization
- Parallelize expensive computations
Examples include:
- Web servers handling multiple client requests
- Gaming engines processing physics and rendering separately
- Operating systems managing background services
Multithreading Example
This example creates multiple threads running simultaneously.
#include <stdio.h>
#include <pthread.h>
void* worker(void* arg)
{
int id = *(int*)arg;
printf("Thread %d is running\n", id);
return NULL;
}
int main()
{
pthread_t threads[3];
int ids[3] = {1, 2, 3};
for (int i = 0; i < 3; i++)
{
pthread_create(
&threads[i],
NULL,
worker,
&ids[i]
);
}
for (int i = 0; i < 3; i++)
{
pthread_join(threads[i], NULL);
}
return 0;
}
Each thread executes independently, potentially at the same time depending on the scheduler and CPU cores.
Where Are Threads Used?
Threads are used everywhere in modern systems.
1. Web Servers
Threads allow servers to handle multiple client requests simultaneously.
Example:
- One thread per connection
- Thread pools for scalability
2. Gaming Engines
Games use threads for:
- Physics calculations
- AI computations
- Rendering
- Audio processing
All running in parallel.
3. Operating Systems
Operating systems use threads for:
- I/O operations
- Background services
- Scheduling tasks
- Device communication
Shared Memory and Synchronization
Threads inside the same process share memory.
This makes communication extremely fast, but it also introduces synchronization problems.
Two major issues are:
- Deadlocks
- Data races
Deadlocks
A deadlock occurs when two or more threads wait indefinitely for each other to release resources.
This creates a circular dependency where no thread can continue execution.
1. Analogy
Imagine two cars blocking each other at an intersection:
- Car A waits for Car B
- Car B waits for Car A
Neither can move.
The same thing happens with threads and locks.
2. Deadlock Example
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
void* thread1_func(void* arg)
{
pthread_mutex_lock(&mutex1);
printf("Thread 1 locked mutex1\n");
sleep(1);
pthread_mutex_lock(&mutex2);
printf("Thread 1 locked mutex2\n");
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
return NULL;
}
void* thread2_func(void* arg)
{
pthread_mutex_lock(&mutex2);
printf("Thread 2 locked mutex2\n");
sleep(1);
pthread_mutex_lock(&mutex1);
printf("Thread 2 locked mutex1\n");
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
return NULL;
}
int main()
{
pthread_t t1, t2;
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
pthread_create(&t1, NULL, thread1_func, NULL);
pthread_create(&t2, NULL, thread2_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
3. What Happens?
- Thread 1 locks
mutex1 - Thread 2 locks
mutex2 - Each thread waits for the other mutex forever
Result:
Deadlock
4. How to Avoid Deadlocks
I. Lock Hierarchy
Always acquire locks in the same predefined order.
Example:
Always lock mutex1 before mutex2
This prevents circular waiting.
II. Timeouts
Use timed locks so threads can give up and retry instead of waiting forever.
Example:
pthread_mutex_timedlock()
Data Races
A data race occurs when:
- Multiple threads access shared data simultaneously
- At least one thread modifies the data
- Access is not synchronized
This leads to unpredictable behavior.
Data Race Example
#include <stdio.h>
#include <pthread.h>
int counter = 0;
void* increment(void* arg)
{
for (int i = 0; i < 100000; i++)
{
counter++;
}
return NULL;
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter = %d\n", counter);
return 0;
}
Expected:
200000
Actual output may vary because both threads modify counter simultaneously.
Mutexes
A mutex acts like a lock that guarantees exclusive access to shared data, thus solving the data race problem.
Mutex Example
#include <stdio.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t mutex;
void* increment(void* arg)
{
for (int i = 0; i < 100000; i++)
{
pthread_mutex_lock(&mutex);
counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Counter = %d\n", counter);
pthread_mutex_destroy(&mutex);
return 0;
}
Now the output becomes deterministic because only one thread can modify the counter at a time.
Conclusion
Mastering threads and multithreading is crucial for building efficient systems, but it’s equally important to understand the risks associated with them. With proper planning and techniques (mutexes, lock hierarchy, ...) you can avoid common pitfalls like deadlocks and data races.