Back to Blog

Multithreaded Programming (Part 1): Thread Creation and Termination

#Multithreading#Programming#thread#null#join

Here, id1 and id2 are pthread_t variables that will hold the IDs of our two new threads. NULL is passed for the attributes, meaning default thread attributes will be used. thread1 and thread2 are the respective functions that each thread will execute, and NULL is passed as the argument to these functions.

Thread Execution and Concurrency

Once threads are created, the operating system's scheduler manages their execution. Threads run concurrently, meaning their execution can interleave in various ways, depending on the scheduler's algorithm, system load, and thread priorities. It's important to remember that "concurrently" doesn't necessarily mean "in parallel" on a single-core system, but rather that their execution progresses independently. On multi-core systems, threads can indeed run in parallel on different CPU cores.

Our example features two thread functions: thread1 and thread2. thread1 is designed to print a message six times, but with a twist: it explicitly exits midway through its loop and includes a sleep call. thread2 is simpler, printing its message three times and then exiting.

The sleep(3) call in thread1 is particularly important for demonstrating concurrency. When thread1 calls sleep(3), it voluntarily relinquishes the CPU for 3 seconds. During this time, the operating system scheduler can allocate CPU time to other ready threads, such as thread2 or even the main thread. This allows us to observe the interleaved execution pattern.

Thread Termination: pthread_exit vs. return

A thread can terminate in several ways:

  1. Returning from its start routine: If the thread's start_routine function simply returns, the thread terminates, and the return value of the function becomes the thread's exit status.
  2. Calling pthread_exit(): This function explicitly terminates the calling thread. It takes a void * argument, which becomes the thread's exit status. pthread_exit() can be called from anywhere within the thread's execution, not just at the end of its start_routine.
  3. Being canceled by another thread: A thread can be terminated by another thread using pthread_cancel().
  4. The entire process terminates: If the main thread or any other thread calls exit(), or if the process encounters an unhandled signal, the entire process (and all its threads) will terminate.

In our example, both thread1 and thread2 use pthread_exit(0) to terminate. thread1 calls pthread_exit(0) when i equals 2. This means thread1 will only print "This is a pthread1." three times (for i=0, 1, 2) before exiting prematurely. thread2 calls pthread_exit(0) after its loop completes, ensuring it prints "This is a pthread2." three times.

Using pthread_exit() is generally preferred over simply returning from the thread function, especially if the thread might terminate from different points within its code, as it explicitly signals thread termination and allows for a consistent exit status. It's crucial to distinguish pthread_exit() from exit(); pthread_exit() terminates only the calling thread, while exit() terminates the entire process.

Waiting for Threads: pthread_join

After creating threads, the main thread (or any other thread) often needs to wait for them to complete their execution. This is where pthread_join comes into play.

The pthread_join function signature is: int pthread_join(pthread_t thread, void **retval);

  • pthread_t thread: The ID of the thread to wait for. This is the ID obtained from pthread_create.
  • void **retval: A pointer to a void * variable where the exit status of the target thread will be stored. If you don't care about the exit status, you can pass NULL.

pthread_join has two primary purposes:

  1. Synchronization: It blocks the calling thread (e.g., the main thread) until the target thread terminates.
  2. Resource Cleanup: When a thread terminates, its resources (like its stack) are not automatically released back to the system. pthread_join reclaims these system resources, preventing "zombie threads" or memory leaks. If a thread is created and allowed to terminate without being joined or detached, its resources remain allocated, leading to resource exhaustion over time.

In our main function, we see: