Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 56

Detecting Thread-Specific Memory Leaks with Valgrind Memcheck

Recommended resources

Hello! Welcome back.

In our last session, we wrapped up our exploration of Valgrind's thread error detectors by looking at DRD. We saw how it complements Helgrind by excelling at finding Pthreads API misuse and performance-sapping lock contention. This completed our tour of tools for finding concurrency-specific bugs.

Today, we'll take one final, crucial step in our debugging module. We'll return to Valgrind's most famous tool, memcheck, and apply it to the unique memory management challenges of multithreaded programs. The learning outcome is to use Valgrind's memcheck to detect memory leaks specific to thread resources (e.g., un-joined threads, un-destroyed mutexes). Even a perfectly synchronized program can be brought down by slow and steady resource leaks, and memcheck is our primary tool for finding them.

A Quick Refresher on Valgrind's memcheck

Given your background, you're likely familiar with using memcheck to find memory leaks from mismatched malloc/free calls in single-threaded programs. The core idea is the same in multithreaded code, but the sources of leaks expand beyond simple malloc calls.

For a very quick refresher on how memcheck presents its findings, you can watch this short segment.

Using Valgrind and GDB together to fix a segfault and memory leak

This video from Mike Shah offers a concise demonstration of using memcheck to find and fix a basic memory leak.

Watch from 05:04 to 08:10. Focus on the command used (valgrind --leak-check=full) and the key sections of the output: the 'HEAP SUMMARY' and the 'LEAK SUMMARY', particularly the 'definitely lost' category and the associated stack trace that pinpoints the allocation site.

The key is running valgrind with the --leak-check=full flag and looking for non-zero values in the leak summary. Now, let's apply this to thread-specific resources.


Leak #1: The Un-joined Thread

When you create a thread with pthread_create, the operating system allocates a significant amount of resources for it, including its own stack space and internal management structures. A common mistake is to create threads and simply let them finish their work without explicitly cleaning up these resources.

Consider this program:

// thread_leak.c
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

void* thread_func(void* arg) {
    printf("Thread is running and will now exit.\n");
    return NULL;
}

int main() {
    pthread_t t1;
    
    printf("Main: Creating a thread.\n");
    pthread_create(&t1, NULL, thread_func, NULL);
    
    // We "forget" to join or detach the thread.
    // Let's sleep for a bit to ensure the thread runs and exits.
    sleep(1); 
    
    printf("Main: Program finished.\n");
    return 0;
}

Let's compile and check this for leaks:

  1. gcc -g -o thread_leak thread_leak.c -pthread
  2. valgrind --leak-check=full ./thread_leak

You will see an output from Valgrind that reports leaked memory. The key part will look something like this, pointing directly at pthread_create:

==12345== LEAK SUMMARY:
==12345==    definitely lost: 0 bytes in 0 blocks
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==    possibly lost: 8,320 bytes in 1 blocks
==12345==    still reachable: 1,152 bytes in 1 blocks
==12345==
==12345== ... bytes in 1 blocks are possibly lost in loss record X of Y
==12345==    at 0x........: calloc (vg_replace_malloc.c:...)
==12345==    by 0x........: _dl_allocate_tls (dl-tls.c:...)
==12345==    by 0x........: pthread_create@@GLIBC_... (allocatestack.c:...)
==12345==    by 0x........: main (thread_leak.c:13)

Valgrind is telling us that memory allocated during pthread_create was never freed. Why? The following resource provides the exact answer.

valgrind memory leak errors when using pthread_create

This Stack Overflow question deals with the precise problem we're observing: Valgrind reporting leaks associated with pthread_create.

Read the user's question to see a Valgrind report very similar to ours. Then, focus on the highest-voted answer, which explains why this happens and what the solutions are.

As the resource explains, a thread's resources are not automatically reclaimed upon its termination. They are held in a zombie-like state so that another thread can retrieve its return value. To clean up these resources, you must explicitly do one of two things:

  1. Join the thread: Call pthread_join(t1, NULL). This synchronizes with the thread's termination and reclaims its resources.
  2. Detach the thread: Call pthread_detach(t1). This tells the system that you will never join this thread, so its resources can be freed automatically as soon as it terminates.

Let's fix our code using pthread_join:

// thread_leak_fixed.c
#include <pthread.h>
#include <stdio.h>

void* thread_func(void* arg) {
    printf("Thread is running and will now exit.\n");
    return NULL;
}

int main() {
    pthread_t t1;
    
    printf("Main: Creating a thread.\n");
    pthread_create(&t1, NULL, thread_func, NULL);
    
    // The fix: Join the thread to reclaim its resources.
    pthread_join(t1, NULL); 
    
    printf("Main: Program finished.\n");
    return 0;
}

Compile and run this new version under memcheck. The leak report will be gone, and you'll see the satisfying message: All heap blocks were freed -- no leaks are possible.


Leak #2: The Dynamically Allocated Mutex

The same principle of manual resource management applies to synchronization primitives like mutexes, condition variables, and read-write locks if you allocate them on the heap.

Consider a situation where you need to malloc a mutex, perhaps as part of a larger dynamically allocated struct.

// mutex_leak.c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    pthread_mutex_t* my_mutex;
    
    // Dynamically allocate the mutex
    my_mutex = (pthread_mutex_t*) malloc(sizeof(pthread_mutex_t));
    if (my_mutex == NULL) { return 1; }
    
    // Initialize and use the mutex...
    pthread_mutex_init(my_mutex, NULL);
    pthread_mutex_lock(my_mutex);
    // Critical section...
    pthread_mutex_unlock(my_mutex);
    
    // Clean up Pthreads resources
    pthread_mutex_destroy(my_mutex);
    
    // But we forget to free the memory!
    
    printf("Finished.\n");
    return 0;
}

A common misconception is that pthread_mutex_destroy also frees the memory pointed to by my_mutex. This is incorrect. Running memcheck on this program will confirm it:

  1. gcc -g -o mutex_leak mutex_leak.c -pthread
  2. valgrind --leak-check=full ./mutex_leak

You will get a definitely lost report for exactly sizeof(pthread_mutex_t) bytes (e.g., 40 bytes on a 64-bit system), originating from the malloc call.

Valgrind Memcheck Output for Memory Leaks
This is a typical Valgrind Memcheck report. Notice how it categorizes leaks ('definitely lost', 'possibly lost') and provides a call stack for each, showing exactly where the leaky allocation occurred. In our case, it would point to our `malloc` call.

Why doesn't pthread_mutex_destroy handle this for us? The answer lies in the design of the Pthreads API.

Do I need to use delete after pthread_mutex_destroy

This Stack Overflow discussion directly addresses whether one needs to free the memory for a dynamically allocated mutex after destroying it.

Read the question and the top answer. The answer provides the crucial piece of logic: pthread_mutex_destroy cannot free the memory because it doesn't know if the mutex was allocated on the stack or the heap.

As the resource clarifies, cleanup is a two-step process for heap-allocated Pthreads objects:

  1. Destroy: Call pthread_mutex_destroy() to release any resources the Pthreads library associated with the object.
  2. Free: Call free() to release the memory that the object itself occupies.

The correct cleanup sequence is:

// Correct cleanup
pthread_mutex_destroy(my_mutex);
free(my_mutex);

Adding the free(my_mutex); call to our program will resolve the memory leak.

Test your understanding!

You are reviewing code and see the following snippet for cleaning up a dynamically allocated mutex.

free(my_mutex);
pthread_mutex_destroy(my_mutex);

Is this correct? If not, what is the potential problem?

Show answer

This is incorrect and represents a major bug. After free(my_mutex) is called, the memory pointed to by my_mutex is returned to the system and is no longer valid for your program to access. The subsequent call to pthread_mutex_destroy(my_mutex) will attempt to access this freed memory, leading to undefined behavior. It could cause a segmentation fault or corrupt the heap. The correct order is always destroy first, then free.


Conclusion

You have now completed your tour of the Valgrind suite for multithreaded debugging. By combining memcheck with Helgrind and DRD, you have a powerful, systematic approach to building correct and robust concurrent applications.

Key Takeaways:

  • Thread Resources Leak: Threads created with pthread_create consume resources that must be reclaimed. Failure to call either pthread_join or pthread_detach will result in a memory leak that memcheck will report.
  • Dynamic Primitives Leak: When you malloc a synchronization primitive like a pthread_mutex_t, you are responsible for freeing its memory.
  • Destroy is Not Free: Functions like pthread_mutex_destroy release internal Pthreads resources but do not free the memory of the object itself.
  • Correct Cleanup Order: For dynamically allocated primitives, you must first call the destroy function, and then call free on the pointer.

Preview of the Next Lesson:
Our focus now shifts from correctness to performance. While Valgrind's DRD can spot contention, dedicated profiling tools are needed to get a complete picture of your application's behavior. In the next module, we will kick off our study of optimization by learning how to profile a multithreaded application using tools like perf to identify performance hotspots.