In the last post, we explored the benefits of using multiple threads instead of separate programs or processes. This is particularly beneficial in terms of easier data sharing. We considered a scenario. In this scenario, a song title typed in a word processor should be visible to the music player. This visibility would allow it to play the song. However, since these are two separate processes, sharing information between them is not straightforward.

We also discussed how, if both were part of the same process running as different threads, this exchange of information would be much simpler. This is a simplified example. It effectively demonstrates the key advantage of threads. Threads provide direct access to shared memory without requiring complex communication mechanisms.

We also introduced ourselves to the pthread (POSIX threads) library, a powerful tool for creating and managing threads in C. The example we examined was as follows:

main_pthread_nojoin.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// Function executed by the thread
void * thread_function() {
    printf("Hello from thread function!\n");
    return NULL;
}

int main() {
    pthread_t p1;  // Thread identifier
    int ret;

    // Creating a new thread
    ret = pthread_create(&p1, NULL, thread_function, NULL);
    if (ret != 0) {   
        printf("Error occurred while trying to create pthread\n");
        exit(1);
    }   
    printf("Bye from main\n");

    return 0;
}

And when we ran this program, we observed the following output:

./main_pthread_nojoin 
Bye from main

And the question we asked was: Where did the message from the thread function go? Why didn’t it appear on the console as expected?

Why Didn’t the thread message Appear?

The issue arises because the main process (parent) does not wait for the newly created thread to complete its execution. As a result, the main function finishes and returns. This situation can potentially cause the process to terminate before the new thread gets a chance to complete its execution.

Threads exist within a process. If the main thread exits and the process terminates, the operating system may clean up all its resources. This includes any running threads. This cleanup can abruptly stop their execution. The following sequence diagram may help in visualizing and understanding the concept more clearly.

We need a way to ensure that the main thread waits for the child threads to finish. This will prevent abrupt termination. This is where pthread_join() comes in, allowing us to properly synchronize child thread completion with the main process.

This is where pthread_join() comes to the rescue. It allows the main thread to wait for the child thread to finish. This ensures that all threads complete their tasks properly before exiting. Without it, there’s no guarantee that newly created threads will run before the program terminates. We’ll explore pthread_join() in more detail next.

pthread_join() function

To ensure that the main process waits for the thread to complete, we will use pthread_join(). This function makes the main function pause execution until the specified thread finishes running. Without it, the main function might terminate too soon. It could end the entire process before the thread gets a chance to execute.

Now, let’s modify our main_pthread_nojoin.c program and create a new program file, main_pthread_join.c, where we introduce pthread_join() in the main() function as shown below.

main_pthread_join.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void * thread_function()
{
    printf("Hello from thread function!\n");

    return NULL;
}

int main()
{
    pthread_t p1; 
    int ret;

    ret = pthread_create(&p1, NULL, thread_function, NULL);

    if(ret != 0)
    {
        printf("Error occured while trying to create pthread\n");
        exit(1);
    }

    pthread_join(p1, NULL); /* main function will be forced to wait here 
                             * for the thread function to return
                             */
    printf("Bye from main\n");

    return 0;
}
Now, if we run the program, we should expect the thread function to print messages on the console. Let's compile and execute the modified program. To compile it, type the following command in the terminal:
gcc main_pthread_join.c -o main_pthread_join -lpthread

This command will compile the program and generate an executable named main_pthread_join. To run the executable, type the following in the command line:

./main_pthread_join 
Hello from thread function!
Bye from main

The following sequence diagram will help you visualize this concept more clearly.

In this post, we explored how threads operate within a process. We discussed ways the main thread can ensure that a newly created thread completes its execution before the program exits. We introduced pthread_create() to spawn threads. We used pthread_join() as a mechanism for the main thread to synchronize with the child thread. This prevents premature termination. This ensures that all threads complete their execution as expected before main() returns or exists.

However, while threads share the same address space, this introduces a new challenge—synchronization between multiple threads accessing shared resources. In the next post, we will dive into the critical section. We will understand what it is and how multiple threads coordinate when sharing memory. We will also discuss the issues that arise, such as race conditions. Stay tuned as we explore how to properly handle these challenges in a multi-threaded environment.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.