Hello! Welcome to the first lesson in our course on C programming with Pthreads.
Given your background in computer science and proficiency with C pointers and memory management, we can dive straight into the practical challenges of multithreading. Today, we'll tackle a fundamental task: how to get data into the threads you create.
The learning outcome for this lesson is to pass multiple arguments to thread functions using struct pointers and manage associated memory. You'll learn why simply passing pointers to local variables is a recipe for disaster and master the standard, robust pattern for safely getting data into your threads.
The void* Dilemma
As you know, pthread_create launches a new thread, and it has the following signature:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
The key limitation here is the start_routine. It's a function pointer that must accept a single void* argument and return a void*. This design keeps the API generic, but it raises an immediate question: what if your thread function needs more than one piece of information to do its job?
A Common Pitfall: Passing Stack Pointers
Let's start by exploring a very common mistake. Imagine you want to create several threads in a loop and give each one a unique number (e.g., its creation index). A tempting, but incorrect, approach is to pass the address of the loop counter variable.
The following video provides an excellent demonstration of why this fails and introduces the core concept needed for the correct solution.
How to pass arguments to threads in C
Watch this video from CodeVault to see a practical demonstration of the race condition that occurs when passing the address of a stack variable to threads.
Please watch from 02:47 to 11:13. Pay close attention to: The initial, incorrect attempt to pass the address of the loop counter i (02:47). The explanation of why all threads end up with the same, final value of i (05:29). The correct solution using malloc to give each thread its own memory (10:32).
As the video clearly shows, the problem is one of memory lifetime and visibility. The loop in the main thread continues to execute and modify i concurrently with the new threads starting up. By the time most threads get scheduled to run and dereference the pointer they received, the value of i in main has already changed, often to its final value after the loop has completed. All threads end up seeing the same, incorrect data.
The key takeaway is that you cannot pass a pointer to data that might be modified or go out of scope in the parent thread before the child thread is guaranteed to have finished using it. The solution, as hinted at in the video, is to use the heap.
The Canonical Solution: A Dynamically Allocated struct
To pass multiple arguments, we combine the heap allocation strategy with a standard C idiom: bundling data into a struct. This is the universally accepted method for passing complex data to a Pthread.
The process is as follows:
- Define a
structthat contains all the data a thread needs. - In the parent thread (before calling
pthread_create), dynamically allocate memory for thisstructon the heap usingmalloc. - Fill the members of the allocated
struct. - Pass the pointer to this
struct(cast tovoid*) as the final argument topthread_create. - In the thread's start routine, cast the
void*argument back to a pointer of yourstructtype. - The thread can now safely access its own unique data.
The following resource provides a clear explanation and code examples of this pattern.
6.5. Thread Arguments and Return Values
This article, 'Thread Arguments and Return Values', provides a comprehensive guide to the standard technique. We'll focus on passing arguments for now.
Please read section '6.5.2. Passing Multiple Arguments to Threads'. Pay close attention to: Code Listing 6.9, which shows how to allocate and populate the struct. The 'Bug Warning' that immediately follows, which explicitly details the problem with using a stack-allocated struct—a crucial concept for memory safety.
Memory Management: Who is Responsible for free?
Since you're allocating memory with malloc, you are responsible for freeing it to prevent memory leaks. Where should free be called?
-
Wrong: Calling
freein the parent thread immediately afterpthread_create. This re-introduces the same race condition: the parent might free the memory before the child thread has a chance to read it, leading to a use-after-free bug. -
Correct: The thread that receives the data is responsible for freeing it. Once the thread has finished using the arguments (e.g., by copying them into its own local variables), it can and should call
freeon the argument pointer it received.
A code snippet from 'Thread Arguments and Return Values' showing the child thread taking responsibility for freeing the dynamically allocated args struct after it has been used.
A More Subtle Race Condition
Your CS background means you'll appreciate a more nuanced bug. It's not just about stack vs. heap; it's about ensuring each thread gets a unique piece of heap memory. Consider this common mistake: allocating a single struct before the creation loop and simply updating and passing the same pointer to every thread.
This is still a race condition! While the memory block is on the heap and won't go out of scope, you are repeatedly overwriting the same block of memory. The first few threads might see the correct data, but later threads will see data intended for other threads.
The following resource clearly illustrates this mistake versus the correct approach.
Pthreads Dev – Common Programming Mistakes to Avoid
This blog post highlights common Pthread mistakes. The section on passing pointers is particularly relevant here.
Focus on the section titled 'Passing pointers to data on the stack to another thread'. Compare the 'Correct' and 'Wrong!' examples under '(Case b) More complex – a data structure:'. Notice how the correct version calls malloc inside the loop, while the incorrect version reuses the same memory block.
Putting It All Together
Let's consolidate everything into a complete, working example. This C program creates two threads. Each thread receives a unique ID, a string, and a prime number to process.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
// 1. Define a struct to hold all the arguments for a thread.
typedef struct {
int thread_id;
char* message;
int prime_number;
} thread_args_t;
// The thread's start routine.
void* thread_function(void* arg) {
// 5. Cast the void* argument back to the struct type.
thread_args_t* args = (thread_args_t*)arg;
printf("Thread %d starting. Message: '%s', Prime: %d\n",
args->thread_id, args->message, args->prime_number);
// Simulate some work
sleep(1);
printf("Thread %d finished.\n", args->thread_id);
// 6. Free the dynamically allocated memory.
// Since message was also dynamically allocated, free it too.
free(args->message);
free(args);
return NULL;
}
int main() {
pthread_t t1, t2;
int primes[] = {13, 17};
printf("Main: Creating threads.\n");
// === Create Thread 1 ===
// 2. Dynamically allocate memory for the arguments.
thread_args_t* args1 = malloc(sizeof(thread_args_t));
if (args1 == NULL) { /* Handle error */ return 1; }
// 3. Fill the struct.
args1->thread_id = 1;
args1->prime_number = primes[0];
args1->message = strdup("Hello from Thread 1"); // strdup also uses malloc
// 4. Create the thread, passing the pointer to the struct.
if (pthread_create(&t1, NULL, thread_function, args1) != 0) {
/* Handle error */ return 1;
}
// === Create Thread 2 ===
thread_args_t* args2 = malloc(sizeof(thread_args_t));
if (args2 == NULL) { /* Handle error */ return 1; }
args2->thread_id = 2;
args2->prime_number = primes[1];
args2->message = strdup("Greetings from Thread 2");
if (pthread_create(&t2, NULL, thread_function, args2) != 0) {
/* Handle error */ return 1;
}
// Wait for both threads to finish. We'll cover this in detail next lesson.
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Main: All threads have completed.\n");
return 0;
}
Compile this code (gcc program.c -o program -pthread) and run it. You'll see each thread receives and prints its own unique data, demonstrating that the pattern works correctly.
Test your understanding!
Modify the code example above. Create an array of 5 pthread_t objects. In a for loop, create 5 threads. Each thread should be passed its index in the loop (0 to 4) and a pointer to a unique string (e.g., "Message for thread 0", "Message for thread 1", etc.). The thread function should simply print the ID and the message it received. Don't forget to correctly manage all dynamically allocated memory.
Show answer
Here is one possible solution. The key changes are placing the creation logic inside a loop and ensuring malloc is called for a new thread_args_t on every iteration.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
typedef struct {
int thread_id;
char* message;
} thread_args_t;
void* thread_function(void* arg) {
thread_args_t* args = (thread_args_t*)arg;
printf("Thread %d received message: '%s'\n", args->thread_id, args->message);
sleep(1);
// Free the memory for the message and the struct itself
free(args->message);
free(args);
return NULL;
}
#define NUM_THREADS 5
int main() {
pthread_t threads[NUM_THREADS];
printf("Main: Creating %d threads.\n", NUM_THREADS);
for (int i = 0; i < NUM_THREADS; i++) {
// Allocate a new argument struct for each thread
thread_args_t* args = malloc(sizeof(thread_args_t));
if (args == NULL) {
perror("Failed to allocate memory for thread args");
exit(EXIT_FAILURE);
}
args->thread_id = i;
// Create a unique message for each thread
char buffer[100];
snprintf(buffer, sizeof(buffer), "Message for thread %d", i);
args->message = strdup(buffer); // strdup is convenient as it mallocs
if (args->message == NULL) {
perror("Failed to strdup message");
exit(EXIT_FAILURE);
}
if (pthread_create(&threads[i], NULL, thread_function, args) != 0) {
perror("Failed to create thread");
exit(EXIT_FAILURE);
}
}
// Wait for all threads to finish
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Main: All threads have completed.\n");
return 0;
}
Conclusion
In this lesson, you've learned the fundamental and safest method for passing data to threads in C.
Key Takeaways:
- To pass multiple arguments to a thread, you must bundle them into a
struct. - The
structinstance must be dynamically allocated on the heap (usingmalloc) to prevent its memory from becoming invalid due to scope changes in the creating thread. - A new, unique memory allocation must be performed for each thread to prevent race conditions where threads overwrite each other's data.
- The thread that receives the allocated
structis typically responsible for callingfreeto prevent memory leaks.
Preview of the Next Lesson:
We've successfully passed data into a thread. But what if the thread needs to send a result back to the parent? In our next lesson, we will explore pthread_join in more depth, focusing on how it allows the parent thread to not only wait for a child to terminate but also to safely retrieve a return value from it.