Asynchronous Cancellation

 

Chapter 32 — Section 32.6
Asynchronous Cancellation
The most powerful — and most dangerous — cancellation mode. Understand why it is almost never safe to use.

32.6a — What Is Asynchronous Cancellation?

With the default DEFERRED type, cancellation fires only at specific cancellation points. With ASYNCHRONOUS type, the rules change dramatically:

DEFERRED (Safe Default)
i = i * 2;
update_array(arr);
write_log();
sleep(1); ← fires HERE only
Cancellation at predictable, safe points
ASYNCHRONOUS (Dangerous)
i = i * ←can cancel here
update_arr←or here
write_←or here
sleep(1)←or here
Cancellation at ANY machine instruction

In asynchronous mode, a pending cancellation may be delivered at any machine-language instruction — not just at designated points. The OS can preempt the thread mid-operation and terminate it.

32.6b — Why Asynchronous Cancellation is Dangerous

Problem 1: Cleanup Handlers Cannot Know the State

In deferred mode, if a thread is canceled at pthread_cond_wait(), we know exactly the thread’s state: the mutex is locked (or being released atomically), buf is allocated. The cleanup handler can rely on this.

With asynchronous cancellation, the thread could be stopped at any point — before malloc(), between malloc() and the lock, or after the lock. The handler has no way to know which steps completed:

/* Thread might be canceled at any of these points: */
←? buf = malloc(0x10000); // before malloc: buf=NULL
/* malloc() itself is not async-cancel-safe → heap corruption possible */
←? pthread_mutex_lock(&mtx); // buf allocated, mtx not locked
←? use_buffer(buf); // buf allocated, mtx locked
/* Cleanup handler cannot distinguish these 3 states! */

Problem 2: Most Library Functions Are Not Async-Cancel-Safe

A thread that is asynchronously cancelable cannot safely call almost any library function, because those functions may have internal state that gets corrupted if the thread is canceled mid-way. This includes:

malloc() / free()
Maintain internal heap structures. Cancellation mid-call → heap corruption.
pthread_mutex_lock()
Mutex stays permanently locked → deadlock for all other threads.
stdio functions
printf(), fprintf() hold internal FILE locks. Cancel mid-call → locked forever.
Most Pthreads calls
pthread_cond_wait(), sem_post(), etc. are not async-cancel-safe.
⚠️ The core rule
An asynchronously cancelable thread must NOT allocate resources, acquire mutexes, semaphores, or locks, and must NOT call any library function that is not explicitly listed as async-cancel-safe. In practice this eliminates almost all useful work.

32.6c — The Only Three Async-Cancel-Safe Functions

SUSv3 explicitly requires only three functions to be async-cancel-safe — safe to call from a thread that uses asynchronous cancellation:

pthread_cancel()
Send a cancellation request to another thread. Safe to call from async-cancelable thread.
pthread_setcancelstate()
Change the thread’s cancellation state. Needed to temporarily protect sections.
pthread_setcanceltype()
Change the cancellation type. Used to switch between DEFERRED and ASYNCHRONOUS.
Why only three?
These three are the minimum needed to manage the cancellation mechanism itself. They have been specifically designed to be safe at the implementation level. Notice that even pthread_testcancel() — which is always a cancellation point — is NOT listed as async-cancel-safe, because it does more complex work.

32.6d — When Is Asynchronous Cancellation Actually Useful?

Given all the dangers, there is exactly one scenario where asynchronous cancellation is genuinely safe and appropriate:

✅ Safe Use Case: Pure Compute-Bound Loop with No Resource Operations
A thread that only does arithmetic computation — no malloc, no mutex, no I/O, no library calls — can safely use asynchronous cancellation. Since there are no resources to clean up, the cleanup handler problem doesn’t exist.
/* This loop is SAFE for async cancellation:
   pure arithmetic, no resources, no library calls */
for (long long i = 0; i < HUGE_NUMBER; i++) {
    result += i * i * i;   /* arithmetic only */
}
/* If canceled mid-loop: nothing to clean up, no corruption */

The practical advantage over using pthread_testcancel() is slightly faster cancellation response — the thread doesn’t need to reach a test point. But given the risks, pthread_testcancel() is almost always the better choice even for compute loops, because it’s safer if the loop ever needs modification.

💡 Practical Recommendation
In real-world code, avoid PTHREAD_CANCEL_ASYNCHRONOUS entirely. Use deferred cancellation with pthread_testcancel() for compute loops. The deferred mode is predictable, debuggable, and extensible. Asynchronous mode is a maintenance hazard — future developers may add a malloc() to the loop not knowing the thread is async-cancelable.

Code Example 1: Enabling Asynchronous Cancellation

Shows how to switch to asynchronous mode for a pure compute loop, then safely switch back to deferred mode before doing any resource operations.

/* async_cancel.c — Async mode for pure compute, then revert */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

static void *
computeThread(void *arg)
{
    int       oldtype;
    long long result = 0;

    /* Phase 1: Switch to ASYNC for pure compute section
       ONLY safe because: no malloc, no mutex, no library calls */
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);

    printf("Thread: starting pure compute (async cancel enabled)\n");

    /* This loop contains only arithmetic — safe for async cancel */
    for (long long i = 0; i < 5000000000LL; i++) {
        result += (i % 1000) * (i % 777);
        /* NO malloc, NO mutex, NO library calls in this loop */
    }

    /* Phase 2: Revert to DEFERRED before any resource operations */
    pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype);

    /* From here: resource operations are safe again */
    printf("Thread: compute done (result=%lld), now doing resource work\n",
           result);

    /* Any resource allocation or I/O goes here in DEFERRED mode */
    sleep(1);   /* cancellation point — any pending cancel fires here */

    printf("Thread: all done\n");
    return (void *)(intptr_t)result;
}

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

    pthread_create(&tid, NULL, computeThread, NULL);

    sleep(1);   /* Thread is in async compute phase */
    printf("Main: canceling thread (it is in async mode)\n");
    pthread_cancel(tid);

    pthread_join(tid, &res);
    printf("Main: thread %s\n",
           res == PTHREAD_CANCELED ? "was canceled" : "finished normally");
    return 0;
}
📌 Safe pattern
The key discipline: switch to ASYNC only for the pure compute section, then immediately revert to DEFERRED before doing anything with resources. Any cancel that was pending during the async section fires at the sleep(1) in deferred mode.

Code Example 2: Demonstrating the Danger — Unsafe Async Usage

This example shows WHY async cancellation is dangerous with malloc(). DO NOT use this pattern — it is shown for educational purposes only.

/* async_danger.c — EDUCATIONAL: shows WHY async cancel is unsafe
   ⚠️  DO NOT use this pattern in real code ⚠️  */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static void
cleanupHandler(void *arg)
{
    /* ❌ Problem: handler can't know if malloc() completed or not.
       If buf is NULL (cancel before malloc), free(NULL) is ok.
       If cancel was INSIDE malloc() — heap is corrupted regardless. */
    printf("cleanup: attempting free(%p) — may be corrupted!\n", arg);
    free(arg);   /* This is unsafe if malloc was mid-call */

    /* ❌ Another problem: mutex may or may not be locked.
       Unlocking an unlocked mutex is undefined behavior. */
    printf("cleanup: attempting mutex unlock — may deadlock!\n");
    pthread_mutex_unlock(&mtx);   /* undefined if not actually locked */
}

static void *
unsafeThread(void *arg)
{
    void *buf = NULL;

    /* ⚠️  WRONG: enable async cancel before resource operations */
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    pthread_cleanup_push(cleanupHandler, buf);  /* buf is NULL here */

    /* Cancel can happen ANYWHERE below — cleanup can't handle it */
    buf = malloc(1024);                 /* ← cancel inside malloc? heap corrupted */
    pthread_mutex_lock(&mtx);           /* ← cancel here? mutex stays locked  */
    /* ... do work ... */
    pthread_mutex_unlock(&mtx);
    free(buf);

    pthread_cleanup_pop(0);
    return NULL;
}

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

    printf("⚠️  This is an UNSAFE demo — do not copy this pattern!\n");
    pthread_create(&tid, NULL, unsafeThread, NULL);
    usleep(100);   /* Cancel almost immediately — likely mid-operation */
    pthread_cancel(tid);
    pthread_join(tid, &res);

    printf("Result: %s (program may be in corrupted state)\n",
           res == PTHREAD_CANCELED ? "CANCELED" : "NORMAL");
    return 0;
}
⚠️ Educational Purpose Only
This code demonstrates the problem — the cleanup handler receives a stale buf=NULL pointer (because cleanup_push was called before malloc), and has no way to know if the mutex was locked. This is the fundamental unsolvable problem with async cancellation and resources.

Interview Questions — Section 32.6

Q1. What is the difference between DEFERRED and ASYNCHRONOUS cancellation?
Answer: With DEFERRED (default), a cancellation request is only acted upon when the thread calls one of the SUSv3 cancellation point functions. With ASYNCHRONOUS, the cancellation may be delivered at any machine instruction — the thread can be stopped mid-operation.
Q2. Why can’t cleanup handlers solve the async-cancellation safety problem?
Answer: With async cancellation, the thread might be canceled before malloc(), inside malloc(), between malloc() and mutex-lock, or after mutex-lock. The cleanup handler receives the same argument regardless of when cancellation occurred and has no way to determine which steps completed. It cannot reliably decide what to free or what to unlock.
Q3. Name the three functions SUSv3 requires to be async-cancel-safe.
Answer: pthread_cancel(), pthread_setcancelstate(), and pthread_setcanceltype(). These are the only Pthreads functions guaranteed safe to call from a thread with PTHREAD_CANCEL_ASYNCHRONOUS.
Q4. When is PTHREAD_CANCEL_ASYNCHRONOUS safe to use?
Answer: Only when a thread performs pure computation — arithmetic only, with absolutely no malloc(), no mutex operations, no semaphores, no library function calls, and no I/O. Even then, pthread_testcancel() in deferred mode is usually preferable for safety and maintainability.
Q5. What happens if a thread calls malloc() while asynchronously cancelable?
Answer: If the thread is canceled inside malloc(), the heap’s internal data structures (free lists, block headers) may be left in a partially updated state. Subsequent malloc() or free() calls in the process — from any thread — can crash or corrupt data. This is why malloc() is not async-cancel-safe.
Q6. Is there a way to “protect” a section from async cancellation?
Answer: Yes — call pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype) before doing resource operations, and restore with pthread_setcanceltype(oldtype, NULL) afterward. Or use pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, ...) to block all cancellation.
© EmbeddedPathashala | TLPI Chapter 32 | Section 32.6

Leave a Reply

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