Thread Termination

Thread Termination
Part 4 of 9  |  Return · pthread_exit() · pthread_cancel() · exit()
Level
Beginner
Function
pthread_exit()
Book
TLPI – Ch 29.4

Every thread that starts must eventually finish. Understanding the different ways a thread can terminate is critical — some methods affect only the calling thread, while others terminate the entire process instantly. Choosing the wrong termination method is a common source of subtle bugs in multithreaded programs.

Key Terms

pthread_exit() return statement pthread_cancel() exit() retval Thread Lifetime Stack Frame

1. The Four Ways a Thread Can Terminate

A thread’s execution ends in exactly one of four ways:

1
Return from the start function

The cleanest way. When the start function executes a return statement, the thread terminates normally. The return value becomes the thread’s “exit status”, retrievable via pthread_join(). This is exactly like main() returning in a single-threaded program.

2
Call pthread_exit()

The thread calls pthread_exit(retval) explicitly. This terminates only the calling thread. Other threads in the process continue running. It is useful when you want to exit the thread from a deeply nested helper function, not just from the start function itself. Think of it as the thread equivalent of calling exit() for a process.

3
pthread_cancel() — cancelled by another thread

Another thread calls pthread_cancel(tid) to cancel this thread. The thread does not stop immediately; cancellation happens at a cancellation point (e.g., inside blocking calls like read(), sleep(), etc.). Cancellation is covered in detail in Chapter 32.

4
Any thread calls exit() — or main() returns

If any thread calls exit(), or if the main thread executes a return in main(), all threads in the process are terminated immediately. This is often surprising for beginners — even detached threads are killed.

2. pthread_exit() — Detailed Explanation

#include <pthread.h>

void pthread_exit(void *retval);
/* Never returns */

pthread_exit() terminates the calling thread. The retval argument specifies the thread’s “return value”, which another thread can collect using pthread_join().

⚠️ Critical Rule: Never return a pointer to a local (stack) variable

The retval you pass to pthread_exit() must not point to anything on the thread’s stack. When the thread terminates, its stack is freed and may be immediately reused by a new thread. Any pointer into that stack becomes a dangling pointer.

Safe options for retval:

  • A simple integer cast: (void *)(intptr_t) status_code
  • A pointer to a heap-allocated structure
  • A pointer to a global variable

Special case — main thread: If the main thread calls pthread_exit() instead of return or exit(), the main thread terminates but the other threads in the process continue executing. The process stays alive until all threads have finished. This is a very useful pattern when you want to fire off worker threads and not block the main thread waiting for them.

3. Comparison: Termination Methods

Method Who Terminates? Other Threads? Return Value?
return Calling thread only Continue Yes (return value)
pthread_exit() Calling thread only Continue Yes (retval arg)
pthread_cancel() Target thread (at cancellation point) Continue PTHREAD_CANCELED
exit() / main() return Entire process All killed immediately No (process ends)

4. Code Example: pthread_exit() vs return

This example demonstrates the difference between using return and pthread_exit(), and shows that calling pthread_exit() from main() lets other threads finish.

/* compile: gcc -pthread -o thread_exit thread_exit.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

static void *slow_worker(void *arg)
{
    int id = (int)(intptr_t) arg;
    printf("Worker %d: started\n", id);
    sleep(2);  /* simulate work */
    printf("Worker %d: finished\n", id);
    return (void *)(intptr_t) id;  /* return thread number as exit code */
}

int main(void)
{
    pthread_t t1, t2;

    pthread_create(&t1, NULL, slow_worker, (void *)(intptr_t) 1);
    pthread_create(&t2, NULL, slow_worker, (void *)(intptr_t) 2);

    printf("Main: threads created, main will now exit via pthread_exit()\n");
    printf("Main: workers will still finish!\n");

    /*
     * pthread_exit() in main — does NOT kill the workers.
     * The process stays alive until t1 and t2 both finish.
     *
     * If we used exit(0) or return 0 here, workers would be killed!
     */
    pthread_exit(NULL);

    /* Code below is never reached */
    printf("This line is never printed.\n");
    return 0;
}

Expected output:

Main: threads created, main will now exit via pthread_exit()
Main: workers will still finish!
Worker 1: started
Worker 2: started
Worker 1: finished
Worker 2: finished

Notice: the main thread exits early but the workers complete. If you change pthread_exit(NULL) to exit(0) or return 0, the workers would be killed before they print “finished”.

5. Code Example: Stack Pointer Danger

This example illustrates what NOT to do — returning a pointer to a local (stack) variable. The code compiles but has undefined behaviour.

/* compile: gcc -pthread -o stack_danger stack_danger.c
   WARNING: This demonstrates a BUG — undefined behaviour */
#include <stdio.h>
#include <pthread.h>

/* WRONG — returns pointer to local variable */
static void *bad_thread(void *arg)
{
    int local_result = 42;  /* on the STACK */

    /* DANGEROUS: stack is freed when thread exits! */
    return &local_result;   /* BUG: dangling pointer */
}

/* RIGHT — returns heap-allocated value */
static void *good_thread(void *arg)
{
    int *result = malloc(sizeof(int));   /* on the HEAP — safe */
    *result = 42;
    return result;   /* caller must free() this */
}

int main(void)
{
    pthread_t t;
    void *res;

    /* WRONG way */
    pthread_create(&t, NULL, bad_thread, NULL);
    pthread_join(t, &res);
    /* res now points to freed stack memory — undefined behaviour */
    printf("bad_thread result (UNDEFINED): %d\n", *(int *)res);

    /* RIGHT way */
    pthread_create(&t, NULL, good_thread, NULL);
    pthread_join(t, &res);
    printf("good_thread result: %d\n", *(int *)res);
    free(res);   /* we own the heap allocation now */

    return 0;
}

The bad_thread example may appear to work sometimes (by luck), but it is a bug. Always use heap allocation or a simple integer cast for thread return values.

6. Interview Questions

Q1. What are the four ways a thread can terminate?
(1) The start function executes a return statement; (2) the thread calls pthread_exit(); (3) the thread is cancelled via pthread_cancel(); (4) any thread in the process calls exit(), or the main thread returns from main(), which terminates all threads.
Q2. What is the difference between a thread calling exit() and calling pthread_exit()?
exit() terminates the entire process — all threads are killed immediately. pthread_exit() terminates only the calling thread; other threads continue running normally.
Q3. Can the return value of a thread point to a local variable? Why or why not?
No. When a thread terminates, its stack is freed and may be reused. A pointer to a local variable on that stack becomes a dangling pointer, leading to undefined behaviour. Use heap-allocated memory, global variables, or simple integer values cast to void * instead.
Q4. What happens if the main thread calls pthread_exit() instead of return or exit()?
Only the main thread terminates. The process remains alive, and all other threads continue executing. The process will terminate naturally when all threads have finished. This is useful for fire-and-forget thread patterns where the main thread has no more work to do.
Q5. When is pthread_exit() preferred over return?
pthread_exit() is useful when you want to terminate the thread from a function that is called by the start function (not from the start function directly). With a simple return, you can only return from the immediate function — you would have to propagate the return through every level of the call stack manually. pthread_exit() exits immediately from wherever it is called.

Next: Part 5 — Thread IDs

Learn how threads are identified: pthread_self(), pthread_equal(), and the difference between POSIX thread IDs and kernel thread IDs.

Leave a Reply

Your email address will not be published. Required fields are marked *