Testing for Thread Cancellation

 

Chapter 32 — Section 32.4
Testing for Thread Cancellation
What happens when a thread never reaches a natural cancellation point? Introducing pthread_testcancel()

32.4a — The Problem: Compute-Bound Loops

Section 32.3 showed that deferred cancellation only fires when a thread reaches a cancellation point — a function like sleep(), read(), or pthread_cond_wait().

But what about a thread doing pure computation — crunching numbers, sorting data, encrypting a file, rendering pixels — with no I/O and no blocking calls? Such a thread may never call any function from the cancellation point list. The cancellation request stays pending forever, and the thread never stops.

❌ Problem: No Cancellation Points in Loop
static void *
computeThread(void *arg)
{
    long long result = 0;

    /* Pure compute loop — NO cancellation points inside */
    for (long long i = 0; i < 10000000000LL; i++) {
        result += i * i;    /* arithmetic only */
    }
    /* pthread_cancel() has no effect — this loop runs to completion
       regardless of how many times you call pthread_cancel() */
    return (void *)result;
}
Even if main() calls pthread_cancel() immediately, this thread will run all 10 billion iterations before the pending request can ever fire. The cancel is completely stuck.

✅ Solution: Manually Introduce Cancellation Points with pthread_testcancel()
The function pthread_testcancel() exists for exactly this purpose. Call it periodically inside any compute-bound loop to create a safe place where a pending cancellation can fire.

32.4b — pthread_testcancel()

#include <pthread.h>
void pthread_testcancel(void);
No parameters. No return value. Its only job: act as a cancellation point.

How It Works — Three Possible Outcomes

Case 1: No pending cancel
pthread_testcancel() checks for a pending cancellation, finds none, and returns immediately. No effect on the thread.
Case 2: Cancel pending, state DISABLED
A cancel is pending but cancellation state is DISABLED. The function returns without acting. The cancel remains pending.
Case 3: Cancel pending, state ENABLED
A cancel is pending and state is ENABLED. The thread terminates here. Any cleanup handlers are invoked first.

Call Frequency — How Often to Call It?

There’s a balance to strike:

Strategy Pros Cons
Every iteration Fastest response to cancel Function call overhead — slows tight loops
Every N iterations (e.g., every 1000) Good balance: low overhead, timely cancel Cancel may be slightly delayed
Never (no testcancel) Zero overhead Cancel never fires — thread runs to completion
💡 Practical Rule
Call pthread_testcancel() often enough that cancellation responds within a reasonable time for your application — typically every few thousand iterations for heavy computation, or at natural “batch boundaries” (e.g., after processing each record).

32.4c — Where to Place pthread_testcancel()

Place pthread_testcancel() at safe points in your loop — where the thread is in a consistent state (no half-completed operations, no held locks). If a cleanup handler must unlock a mutex, ensure the mutex is either held or not held — never mid-acquisition.

❌ Bad Placement — Mid-Operation
pthread_mutex_lock(&mtx);
pthread_testcancel(); /* BAD: mutex held! */
/* If canceled here, mutex stays locked */
do_work();
pthread_mutex_unlock(&mtx);
✅ Good Placement — Between Operations
pthread_mutex_lock(&mtx);
do_work();
pthread_mutex_unlock(&mtx);
pthread_testcancel(); /* GOOD: no lock held */
/* Safe state: cleanup has nothing to undo */

Code Example 1: pthread_testcancel() in a Compute Loop

Sum-of-squares computation thread. Without testcancel(), the cancel would never fire. With it, the thread responds promptly.

/* testcancel_compute.c — pthread_testcancel() in a compute loop */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

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

    printf("Compute: started (10 billion iterations)\n");

    for (i = 0; i < 10000000000LL; i++) {
        result += i * i;

        /* Check for cancellation every 1 million iterations
           — low overhead, yet responds within ~10ms */
        if (i % 1000000 == 0) {
            pthread_testcancel();   /* Acts as cancellation point */
        }
    }

    printf("Compute: finished, result=%lld\n", result);
    return (void *)result;
}

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

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

    sleep(1);   /* Let thread start and run a bit */

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

    pthread_join(tid, &res);

    if (res == PTHREAD_CANCELED)
        printf("Main: compute thread was canceled successfully\n");
    else
        printf("Main: compute thread finished normally\n");

    return 0;
}
Compute: started (10 billion iterations)
Main: canceling compute thread
Main: compute thread was canceled successfully
# Thread is canceled after ~1 second, not after 10 billion iterations
📌 Key point
Without the pthread_testcancel() call inside the loop, the cancel would be pending but the thread would run all 10 billion iterations regardless. With it, the thread terminates at the next test point after the cancel request arrives.

Code Example 2: Batch Processing with Periodic Cancellation Check

Processing records from an array. testcancel() called at batch boundaries — a more realistic pattern for data-processing threads.

/* batch_testcancel.c — testcancel at batch boundaries */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define TOTAL_RECORDS   50
#define BATCH_SIZE      10

static void
process_record(int rec_num)
{
    /* Simulate processing without any blocking/cancellation point */
    volatile long sum = 0;
    for (long j = 0; j < 500000; j++) sum += j;
    (void)sum;
}

static void *
processorThread(void *arg)
{
    int i, processed = 0;

    printf("Processor: starting %d records in batches of %d\n",
           TOTAL_RECORDS, BATCH_SIZE);

    for (i = 0; i < TOTAL_RECORDS; i++) {
        process_record(i);
        processed++;

        /* After every complete batch, check for cancellation */
        if (processed % BATCH_SIZE == 0) {
            printf("Processor: batch %d/%d done, checking cancel...\n",
                   processed / BATCH_SIZE, TOTAL_RECORDS / BATCH_SIZE);
            pthread_testcancel();   /* Cancellation point between batches */
        }
    }

    printf("Processor: all %d records processed\n", processed);
    return (void *)(intptr_t)processed;
}

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

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

    /* Cancel after a short delay — thread should be mid-processing */
    usleep(80000);  /* 80ms */

    printf("Main: sending cancel request\n");
    pthread_cancel(tid);

    pthread_join(tid, &res);

    if (res == PTHREAD_CANCELED)
        printf("Main: processor was canceled mid-batch\n");
    else
        printf("Main: processor completed all %ld records\n", (long)res);

    return 0;
}
Processor: starting 50 records in batches of 10
Processor: batch 1/5 done, checking cancel…
Processor: batch 2/5 done, checking cancel…
Main: sending cancel request
Processor: batch 3/5 done, checking cancel…
Main: processor was canceled mid-batch
# Canceled at batch boundary — never mid-record
📌 Why batch boundaries are good cancel points
Placing pthread_testcancel() between batches guarantees that individual records are always processed completely. You never cancel half-way through a record update, which could leave data in an inconsistent state.

Code Example 3: Side-by-Side — With and Without testcancel()

Two threads run simultaneously. Thread A has testcancel(), Thread B does not. Only Thread A responds to cancel.

/* compare_testcancel.c — With vs without testcancel */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

/* Thread A: HAS pthread_testcancel() — responds to cancel */
static void *
threadA(void *arg)
{
    for (long long i = 0; ; i++) {
        if (i % 500000 == 0)
            pthread_testcancel();   /* Has cancel point */
    }
    return NULL;
}

/* Thread B: NO pthread_testcancel() — cancel stays pending forever */
static void *
threadB(void *arg)
{
    for (long long i = 0; i < 3000000000LL; i++) {
        /* No testcancel — cancel pending but never fires */
        (void)i;
    }
    printf("ThreadB: finished all iterations (cancel was ignored!)\n");
    return NULL;
}

int
main(void)
{
    pthread_t tidA, tidB;
    void *resA, *resB;

    pthread_create(&tidA, NULL, threadA, NULL);
    pthread_create(&tidB, NULL, threadB, NULL);

    sleep(1);

    printf("Main: canceling both threads\n");
    pthread_cancel(tidA);
    pthread_cancel(tidB);

    pthread_join(tidA, &resA);
    printf("ThreadA: %s\n", resA == PTHREAD_CANCELED ?
           "CANCELED (testcancel worked)" : "finished normally");

    pthread_join(tidB, &resB);
    printf("ThreadB: %s\n", resB == PTHREAD_CANCELED ?
           "CANCELED" : "finished normally (cancel was ignored)");

    return 0;
}
Main: canceling both threads
ThreadA: CANCELED (testcancel worked)
ThreadB: finished all iterations (cancel was ignored!)
ThreadB: finished normally (cancel was ignored)

Interview Questions — Section 32.4

Q1. What is the purpose of pthread_testcancel()?
Answer: Its only purpose is to act as a cancellation point. A thread calls it periodically inside compute-bound loops that contain no natural cancellation points. If a cancellation is pending and cancellation is enabled, the thread terminates at this call.
Q2. What is the signature of pthread_testcancel() and what does it return?
Answer: void pthread_testcancel(void); — takes no parameters and returns nothing (void). If cancellation is pending and enabled, it does not return at all — the thread terminates.
Q3. If cancellation state is DISABLED, what does pthread_testcancel() do?
Answer: Nothing — it returns immediately without effect. The pending cancellation remains pending. It will only fire once cancellation is re-enabled and the thread reaches a cancellation point.
Q4. Why not just use PTHREAD_CANCEL_ASYNCHRONOUS instead of pthread_testcancel() for compute loops?
Answer: Asynchronous cancellation is dangerous — the thread can be stopped at any machine instruction, potentially in the middle of malloc(), a mutex lock, or other non-reentrant operations. pthread_testcancel() gives you control over where cancellation can occur, making cleanup handlers reliable.
Q5. Where should you NOT place pthread_testcancel()?
Answer: Never inside a critical section where a mutex is held, or in the middle of a multi-step operation that must be atomic. If cancelled there, the cleanup handler may not know the mutex is held, leading to deadlock. Place it between complete operations — after releasing locks, after completing a record update, etc.
Q6. Is pthread_testcancel() listed in the SUSv3 required cancellation points?
Answer: Yes. pthread_testcancel() is explicitly listed in SUSv3’s Table of required cancellation points. It is the only function whose sole purpose is to be a cancellation point — all other functions in the list have primary non-cancellation purposes.

© EmbeddedPathashala | TLPI Chapter 32 | Section 32.4

Leave a Reply

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