×

Search anything:

Linux threads: cancellation, data and cleanup

Free book on Graph Algorithms

Get this book -> Problems on Array: For Interviews and Competitive Programming

A thread is the basic unit of processor utilization that consists of a program counter, stack and registers. In this article we discuss how they are cancelled, how data is handled within threads and cleaning up to prevent memory leaks when a thread terminates or is cancelled.

Table of contents.

  1. Introduction.
  2. Cancelling a thread.
  3. Thread data.
  4. Cleanup
  5. Summary.
  6. References.

Prerequisites.

  1. Linux processes: listing, creation, scheduling
  2. Linux processes: signals, termination, zombies, cleanup
  3. Linux threads: Creation, Data passing, Waiting.

Introduction.

Normally a thread will terminate when it exits, either by returning or pthread_exit being called.

In this article we discuss thread cancelling whereby a thread is given the ability to terminate another thread, synchronous and asynchronous threads, how data is handled between threads and cleaning up after threads using cleanup functions after they terminate or are cancelled.

Cancelling a thread.

To cancel a thread the pthread_cancel function is called, this function takes a thread ID of the thread to be cancelled.

When a thread is cancelled it is joined so as to release the resources it was using, unless it was detached.

The returned value of a cancelled thread is a special value given by PTHREAD_CANCELLED.

A thread can also control when and whether it can be cancelled so as to prevent a situation whereby it is cancelled without deallocating the resources it was using which may lead to a leakage.

When a thread is created, it is in an uncancelable state whereby attempts to cancel it are ignored, it can also be in a synchronously cancelable state whereby it can only be cancelled when it reaches a specified execution point or a asynchronously cancelable state where it can be cancelled at any point during execution.

Synchronous and asynchronous threads.

These points where a thread can be cancelled are referred to as cancellation points. Threads queue cancellation requests until such points are reached.

pthread_setcanceltype function is used to make threads asynchronously cancellable, It's first argument is PTHREAD_CANCEL_ASYNCHRONOUS or PTHREAD_CANCEL_DEFERRED to make a thread asynchronously or synchronously cancellable respectively.

The second argument can be null, if not, it is a pointer to a variable which receives the previous cancellation type for the thread.

To create a cancellation point, pthread_testcancel is used, this function processes a pending cancellation in a synchronously cancellable thread.

A thread can also disable cancellation by using the pthread_setcancelstate function, its first argument can be PTHREAD_CANCEL_DISABLE to disable cancellation or PTHREAD_CANCEL_ENABLE to re-enable cancellation.

A critical section is a sequence of code that must be executed fully or not at all, i.e if its execution begins it must complete without being cancelled.
The pthread_setcancelstate function assists in implementing such cases.

An example
We have a banking system routine to transfer money, if the thread running this routine is cancelled during a transfer of funds, errors in the balances might occur. In such cases we place both the sending operation and receiving operation in critical sections.

#include<pthread.h>
#include<stdio.h>
#include<string.h>

float balance;

int processTransaction(int fromAcc, int toAcc, float amt){
    int oldCancelState;

    if(balance[fromAcc] < amt)
      return 1;
      
    // start critical section
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldCancelState);
    
    // move money
    balance[toAcc] += amt;
    balance[fromAcc] -= amt;
    
    //end critical section
    pthread_setcancelstate(oldCancelState, NULL);

    return 0;
}

Thread data.

Threads in a single program share the same address space, meaning that if a thread modifies a memory location, this change is visible to other threads in the process.
This allows threads to operate on the same data.

Since each thread has its own call stack, it will execute different parts of the code and return from subroutines normally.

In Linux a thread-specific data area stores common data that is duplicated for use among threads. Each thread can modify its copy without affecting other threads.
Thread specific data items are of type void and and each is referenced by a key.
The pthread_key_create function is used to create new keys and data for a thread.
It's first argument is a pointer to pthread_key_t. The key values is used by a thread to access it's copy of the data item.

It's second argument is cleanup function, if a function pointer is passed here, Linux will call it when each thread exits thereby passing the thread-specific value corresponding to that key.

If the thread-specific value is null, the cleanup function is not called otherwise if not null the cleanup function is called even if the thread is cancelled at some point during execution.

An example

An application divides tasks among multiple threads and each thread is to have a different log file whereby thread's progress messages are stored. A thread specific data area is a good place to store the file pointer for the log file for each individual thread.

Implementation

#include<malloc.h>
#include<pthread.h>
#include<stdio.h>

// key to associate a log file pointer with a thread
static pthread_key_t thread_log_key;

// Write message to log file for current thread.
void write_to_thread_log(const char* message){
    FILE* thread_log = (FILE*) pthread_getspecific(thread_log_key);
    fprintf(thread_log, "%s \n", message);
}

// Close log file pointer THREAD_LOG.
void close_thread_log(void* thread_log){
    fclose ((FILE*) thread_log);
}

void* thread_function(void* args){
    char thread_log_filename[20];
    FILE* thread_log;
    
    // Generate filename for this thread's log file.
    sprintf(thread_log_filename, "thread%d.log", (int)pthread_self());
    
    // Open log file.
    thread_log = fopen(thread_log_filename, "w");
    
    // Store file pointer in thread-specific data under thread_log_key.
    pthread_setspecific(thread_log_key, thread_log);
    write_to_thread_log ("Thread starting.");
    
    /* Do work here... */
    
    return NULL;
}
int main(){
    int i;
    pthread_t threads[5];
    
    // Create a key to associate thread log file pointers in thread-specific data. 
    // Use close_thread_log to clean up the file pointers.
    pthread_key_create(&thread_log_key, close_thread_log);
    
    
    // Create threads to do the work.
    for(i = 0; i < 5; ++i)
        pthread_create(&(threads[i]), NULL, thread_function, NULL);
        
    // Wait for threads to finish.
    for (i = 0; i < 5; ++i)
        pthread_join(threads[i], NULL);
        
    return 0;
}

The main function creates a key to store thread specific file pointer and stores it in thread_log_key. This is a global variable and hence shared by all threads.
Each thread during execution open a log file and stores the file pointer under that key.

write_to_thread function may later be called by any of the threads so as to write a message to the thread-specific log file, it also retrieves the file pointer for the thread's log file from thread-specific data ans writes the message.

Cleanup.

Cleanup functions are used to ensure there are no leaks when a thread exits or is cancelled.

Cleanup handlers provided by Linux, are used to specify cleanup functions without having to create a new thread-specific data item that is duplicated for each thread.
Cleanup handlers are called when a thread exits, they take a void* parameter and its argument value is provided when it is registered hence convenient to use it for multiple resources instances.

The pthread_cleanup_push function is used to register a cleanup handler. When it is called it must be balanced with pthread_cleanup_pop function to unregister it.

An example

#include<malloc.h>
#include<pthread.h>

// Allocate temporary buffer.
void* allocate_buffer(size_t size){
    return malloc(size);
}

// Deallocate temporary buffer.
void deallocate_buffer(void* buffer){
    free(buffer);
}

void do_some_work(){
    // Allocate a temporary buffer.
    void* temp_buffer = allocate_buffer(1024);
    
    // Register cleanup handler for buffer.
    // to deallocate it in case the thread exits or is cancelled.
    pthread_cleanup_push (deallocate_buffer, temp_buffer);
    
    /* Work that might call pthread_exit or might be cancelled. */
    
    // Unregister cleanup handler. 
    // This performs cleanup by calling deallocate_buffer, since we pass a nonzero
    pthread_cleanup_pop(1);
}

The program demonstrates how a cleanup handler is used to make sure a dynamically allocated buffer is cleaned up if the thread terminates.

Summary.

Threads allow a program to do multiple tasks at a time.

A process creates threads and threads can also create other threads all which run a single program each executing a different part of the program.

A thread can either be uncancellable, synchronously cancellable or asynchronously cancellable.

Threads in a single program execution share the same address space.
Cleanup function ensure there are no memory leaks when a thread terminates or is cancelled.

References.

  1. Advanced Programming in the UNIX Environment 3rd Edition W. Richard Stevens
  2. Advanced Linux Programming Mark Mitchell, Jeffrey Oldham,and Alex Samuel.
Linux threads: cancellation, data and cleanup
Share this