Threads, Deadlocks, and Data Races in C

@sadenbouuu|September 21, 2024

Where concurrency introduces complexity.

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 thread
  • pthread_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.

GitHubXLinkedIndev.toMediumRSS