Cleanup Handlers

 

Chapter 32 — Section 32.5
Cleanup Handlers
Functions that run automatically when a thread is canceled — to unlock mutexes, free memory, and restore consistent program state

32.5a — Why Do We Need Cleanup Handlers?

Thread cancellation is dangerous without cleanup. Consider a thread that:

🔒 Holds a Mutex
If canceled while holding a mutex, that mutex stays locked forever. All other threads trying to acquire it will deadlock.
💾 Allocated Memory
If canceled after malloc() but before free(), that memory leaks for the lifetime of the process.
📝 Opened a File
If canceled with an open file descriptor or half-written data, the file may be corrupted or the fd leaks.

Cleanup handlers are programmer-defined functions that are automatically invoked when a thread is canceled (or calls pthread_exit()). They let you undo partial work and restore consistent state.

❌ Without Cleanup Handler
pthread_mutex_lock(&mtx);
buf = malloc(1024);
← CANCELED HERE
free(buf);
pthread_mutex_unlock(&mtx);
Mutex locked forever + memory leaked
✅ With Cleanup Handler
pthread_cleanup_push(handler, buf);
pthread_mutex_lock(&mtx);
buf = malloc(1024);
← CANCELED HERE
→ handler() auto-called: free(buf) + unlock
Clean shutdown — no leaks, no deadlocks

32.5b — The Cleanup Handler Stack

Each thread maintains its own stack of cleanup handlers. When a thread is canceled (or calls pthread_exit()), the handlers are called in LIFO order — last pushed is first called.

Cleanup Handler Stack (LIFO)
handler_C ← pushed last
called 1st
handler_B
called 2nd
handler_A ← pushed first
called 3rd
Stack Bottom

This LIFO order mirrors the nesting of code — resources acquired last should be released first (just like C++ destructors and RAII). After all handlers complete, the thread terminates.

32.5c — The API: push and pop

#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void*), void *arg);
void pthread_cleanup_pop(int execute);
Both return void. On Linux these are implemented as macros.

pthread_cleanup_push() — Adding a Handler

Parameter Type Description
routine void (*)(void*) Pointer to the cleanup function. Must have signature void handler(void *arg)
arg void * Argument passed to routine when invoked. Can be a pointer to any data (cast as needed).

The cleanup function must have this exact signature:

void
myCleanupHandler(void *arg)
{
    /* arg is the value passed to pthread_cleanup_push() */
    MyResource *res = (MyResource *)arg;   /* cast as needed */
    free(res->buffer);
    pthread_mutex_unlock(&res->lock);
}

pthread_cleanup_pop() — Removing a Handler

execute value What Happens
nonzero (e.g. 1) Removes the handler from the stack AND executes it. Use when you want cleanup to run regardless of whether you were canceled.
zero (0) Removes the handler from the stack without executing it. Use when the cleanup is no longer needed (e.g., you already freed the resource manually).

32.5d — ⚠️ Critical: The Macro Brace-Pairing Rule

On Linux (and many other implementations), pthread_cleanup_push() and pthread_cleanup_pop() are implemented as macros that expand to code containing an opening brace { and a closing brace } respectively.

This has an important consequence: every push must be paired with exactly one pop in the same lexical block.

❌ ILLEGAL: pop inside if-block
pthread_cleanup_push(func, arg);
...
if (condition) {
    pthread_cleanup_pop(0); /* WRONG */
}
/* Braces don't match — compile error
   or undefined behavior */
✅ CORRECT: same lexical block
pthread_cleanup_push(func, arg);
...
if (condition) {
    /* do conditional work */
}
pthread_cleanup_pop(0); /* CORRECT */
/* push and pop at same nesting
   level — braces match */
📌 Also note: Variable scope
Variables declared between a pthread_cleanup_push() and its matching pthread_cleanup_pop() are limited to that lexical scope due to the macro brace expansion. Declare variables you’ll need in the cleanup handler before the push call.

32.5e — Cleanup Handlers and pthread_exit()

Cleanup handlers are invoked not only when a thread is canceled, but also when a thread calls pthread_exit(). This is extremely useful — a thread can call pthread_exit() to terminate early, and any cleanup handlers that were pushed but not yet popped will still run automatically.

How Thread Terminates Cleanup Handlers Called?
pthread_cancel() → reaches cancel point ✅ YES — all unpoped handlers run
pthread_exit() ✅ YES — all unpoped handlers run
return from thread start function ❌ NO — handlers NOT called on plain return
⚠️ Important: return vs pthread_exit()
If a thread simply does return NULL;, cleanup handlers are not automatically invoked. Only cancellation and pthread_exit() trigger them. If you want cleanup to always run, either use pthread_cleanup_pop(1) before returning, or use pthread_exit() instead of return.

Code Example 1: TLPI Listing 32-2 — Complete Cleanup Handler Example

Thread allocates memory and locks a mutex. Cleanup handler frees memory and unlocks mutex — called both on cancellation and on normal flow via pthread_cleanup_pop(1).

/* thread_cleanup.c — Modeled after TLPI Listing 32-2 */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

static pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t mtx  = PTHREAD_MUTEX_INITIALIZER;
static int glob = 0;   /* predicate variable for condition wait */

/* -----------------------------------------------------------
   Cleanup handler: frees buffer and unlocks mutex.
   Called automatically on cancellation or pthread_cleanup_pop(1).
   ----------------------------------------------------------- */
static void
cleanupHandler(void *arg)
{
    int s;
    printf("cleanup: freeing buffer at %p\n", arg);
    free(arg);                              /* free allocated buffer */

    printf("cleanup: unlocking mutex\n");
    s = pthread_mutex_unlock(&mtx);        /* unlock held mutex */
    if (s != 0) perror("pthread_mutex_unlock");
}

/* -----------------------------------------------------------
   Thread function
   ----------------------------------------------------------- */
static void *
threadFunc(void *arg)
{
    int   s;
    void *buf;

    /* Step 1: allocate memory (not a cancellation point) */
    buf = malloc(0x10000);
    printf("thread: allocated buffer at %p\n", buf);

    /* Step 2: lock mutex (not a cancellation point) */
    s = pthread_mutex_lock(&mtx);
    if (s != 0) perror("pthread_mutex_lock");

    /* Step 3: install cleanup handler AFTER acquiring resources
       arg = buf, so handler knows what to free */
    pthread_cleanup_push(cleanupHandler, buf);

    /* Step 4: wait on condition variable (CANCELLATION POINT)
       — if canceled here, cleanupHandler is auto-called */
    while (glob == 0) {
        s = pthread_cond_wait(&cond, &mtx);   /* cancel point */
        if (s != 0) perror("pthread_cond_wait");
    }
    printf("thread: condition wait loop completed\n");

    /* Step 5: pop handler with execute=1 → runs even on normal exit */
    pthread_cleanup_pop(1);

    return NULL;
}

int
main(int argc, char *argv[])
{
    pthread_t thr;
    void *     res;
    int        s;

    s = pthread_create(&thr, NULL, threadFunc, NULL);
    if (s != 0) { perror("pthread_create"); exit(EXIT_FAILURE); }

    sleep(2);   /* give thread time to start and enter wait */

    if (argc == 1) {
        /* No argument → cancel the thread */
        printf("main: canceling thread\n");
        s = pthread_cancel(thr);
        if (s != 0) perror("pthread_cancel");
    } else {
        /* Argument supplied → signal condition variable normally */
        printf("main: signaling condition variable\n");
        glob = 1;
        s = pthread_cond_signal(&cond);
        if (s != 0) perror("pthread_cond_signal");
    }

    s = pthread_join(thr, &res);
    if (s != 0) { perror("pthread_join"); exit(EXIT_FAILURE); }

    if (res == PTHREAD_CANCELED)
        printf("main: thread was canceled\n");
    else
        printf("main: thread terminated normally\n");

    exit(EXIT_SUCCESS);
}

$ ./thread_cleanup (no arg → cancel path)
thread: allocated buffer at 0x…
main: canceling thread
cleanup: freeing buffer at 0x…
cleanup: unlocking mutex
main: thread was canceled
$ ./thread_cleanup x (arg → normal path)
thread: allocated buffer at 0x…
main: signaling condition variable
thread: condition wait loop completed
cleanup: freeing buffer at 0x…
cleanup: unlocking mutex
main: thread terminated normally
📌 Key observation
The cleanup handler runs in both paths — when the thread is canceled AND when it finishes normally (via pthread_cleanup_pop(1)). This is great design: write cleanup once, get safety in both cases.

Code Example 2: Multiple Cleanup Handlers — LIFO Order

Three cleanup handlers are pushed. When cancelled, they execute in reverse order — demonstrating LIFO behavior.

/* multi_cleanup.c — Multiple handlers execute in LIFO order */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

static void handlerA(void *arg) { printf("  handlerA: releasing %s\n", (char *)arg); }
static void handlerB(void *arg) { printf("  handlerB: releasing %s\n", (char *)arg); }
static void handlerC(void *arg) { printf("  handlerC: releasing %s\n", (char *)arg); }

static void *
threadFunc(void *arg)
{
    printf("Thread: pushing handlers A, B, C (will be called C, B, A)\n");

    pthread_cleanup_push(handlerA, "resource-A (file)");       /* pushed 1st */
    pthread_cleanup_push(handlerB, "resource-B (mutex)");     /* pushed 2nd */
    pthread_cleanup_push(handlerC, "resource-C (buffer)");    /* pushed 3rd */

    printf("Thread: all handlers pushed, now sleeping (cancel point)\n");
    sleep(10);   /* cancellation point — cancel fires here */

    /* If not canceled, pop all handlers */
    pthread_cleanup_pop(0);   /* pop C without executing */
    pthread_cleanup_pop(0);   /* pop B without executing */
    pthread_cleanup_pop(0);   /* pop A without executing */

    printf("Thread: finished normally\n");
    return NULL;
}

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

    pthread_create(&tid, NULL, threadFunc, NULL);
    sleep(1);

    printf("Main: canceling thread\n");
    pthread_cancel(tid);

    pthread_join(tid, &res);
    printf("Main: thread %s\n",
           res == PTHREAD_CANCELED ? "was canceled" : "finished normally");
    return 0;
}
Thread: pushing handlers A, B, C (will be called C, B, A)
Thread: all handlers pushed, now sleeping (cancel point)
Main: canceling thread
handlerC: releasing resource-C (buffer)
handlerB: releasing resource-B (mutex)
handlerA: releasing resource-A (file)
Main: thread was canceled
# LIFO: C first (pushed last), then B, then A (pushed first)

Interview Questions — Section 32.5

Q1. What is a cleanup handler and why is it needed?
Answer: A cleanup handler is a user-defined function automatically called when a thread is canceled or calls pthread_exit(). It is needed to avoid resource leaks — if a thread is canceled while holding a mutex or with allocated memory, the cleanup handler frees those resources and ensures consistent state for the rest of the process.
Q2. In what order are multiple cleanup handlers called?
Answer: LIFO (Last-In, First-Out) — the most recently pushed handler is called first, then the next most recent, and so on down to the first-pushed handler. This mirrors resource acquisition order: the last acquired resource should be released first.
Q3. What does pthread_cleanup_pop(0) vs pthread_cleanup_pop(1) do?
Answer: pop(0) removes the top handler from the stack without calling it — use when cleanup is no longer needed. pop(1) removes the handler and also executes it — use when you want cleanup to happen regardless of whether you were canceled.
Q4. Why must pthread_cleanup_push() and pthread_cleanup_pop() be in the same lexical block?
Answer: On Linux and many implementations they are macros that expand to include an opening brace { (push) and closing brace } (pop). Mismatching their scope causes compilation errors or undefined behavior due to unbalanced braces.
Q5. Are cleanup handlers called when a thread does a plain return?
Answer: No. Plain return from the thread start function does NOT invoke cleanup handlers. Only cancellation and pthread_exit() trigger them automatically. To handle cleanup on return, call pthread_cleanup_pop(1) before returning, or use pthread_exit().
Q6. What is the signature that a cleanup handler function must have?
Answer: void handler(void *arg) — takes a single void * parameter (the value passed as the second argument to pthread_cleanup_push) and returns void. Inside the handler, cast the void * to the appropriate type.
© EmbeddedPathashala | TLPI Chapter 32 | Section 32.5

Leave a Reply

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