Ensuring Thread Safety in C: Thread-Specific Storage (TSS) vs. Thread-Local Storage (TLS)


What is tss_create?

  • It's used to create a new thread-specific storage (TSS) key.
  • tss_create is a function defined in the <threads.h> header file, which is part of the C POSIX (Portable Operating System Interface) threads library.

What is Thread-Specific Storage (TSS)?

  • This is useful for storing data that needs to be unique to each thread, such as thread-local variables, cancellation flags, or temporary data structures.
  • TSS is a memory area that allows each thread in a program to have its own private copy of data associated with the same key.

How tss_create Works

  1. You call tss_create and pass it a pointer to a key_t variable.
  2. tss_create allocates a new TSS key and stores its identifier in the key_t variable you provided.
  3. Each thread can then use the key to store and retrieve its own thread-specific data using the functions tss_set and tss_get.

Benefits of TSS

  • Flexibility
    Threads can store different types of data under the same key, providing flexibility in how thread-specific information is managed.
  • Thread Safety
    Data associated with a TSS key is private to each thread, eliminating the need for complex synchronization mechanisms like mutexes when accessing the data from different threads.

Example

#include <stdio.h>
#include <threads.h>

tss_key_t key;

void *thread_func(void *arg) {
    int value = (int)arg;
    tss_set(key, &value);  // Store thread-specific value

    printf("Thread %d: Value = %d\n", (int)pthread_self(), *(int *)tss_get(key));

    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    int a = 10, b = 20;

    tss_create(&key, NULL);  // Create a thread-specific storage key

    pthread_create(&thread1, NULL, thread_func, (void *)a);
    pthread_create(&thread2, NULL, thread_func, (void *)b);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    tss_delete(key);  // Free the TSS key when no longer needed

    return 0;
}
  • While TSS simplifies thread-specific data management, it's generally considered less flexible and scalable than thread-local storage (TLS) mechanisms available in some C implementations. If your compiler supports TLS, it might be a better choice for complex thread-specific data requirements.
  • TSS keys are global resources, so it's crucial to manage their creation and deletion properly to avoid memory leaks. Use tss_delete to release a TSS key when it's no longer needed.


Storing Cancellation Flags

#include <stdio.h>
#include <threads.h>

tss_key_t cancel_flag_key;

void *thread_func(void *arg) {
    int *should_cancel = (int *)tss_get(cancel_flag_key); // Check cancellation flag

    while (!(*should_cancel)) {
        // Do some work
        printf("Thread %d: Working...\n", (int)pthread_self());
    }

    printf("Thread %d: Cancelled.\n", (int)pthread_self());

    return NULL;
}

int main() {
    pthread_t thread;
    int cancel = 0;

    tss_create(&cancel_flag_key, NULL);  // Create TSS key for cancellation flag

    pthread_create(&thread, NULL, thread_func, NULL);

    // Simulate cancellation request from another thread
    cancel = 1;
    tss_set(cancel_flag_key, &cancel);

    pthread_join(thread, NULL);

    tss_delete(cancel_flag_key);

    return 0;
}

In this example, each thread checks a cancellation flag stored using TSS before proceeding with its work. When cancellation is requested, the flag is set, and the thread gracefully terminates.

Thread-Local Buffers

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>

#define BUFFER_SIZE 1024

tss_key_t buffer_key;

void *thread_func(void *arg) {
    char *buffer = (char *)tss_get(buffer_key);

    if (!buffer) {
        buffer = malloc(BUFFER_SIZE);
        tss_set(buffer_key, buffer);  // Allocate and store thread-local buffer
    }

    strncpy(buffer, (char *)arg, BUFFER_SIZE);
    printf("Thread %d: Message: %s\n", (int)pthread_self(), buffer);

    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    const char *message1 = "Hello from thread 1";
    const char *message2 = "Greetings from thread 2";

    tss_create(&buffer_key, NULL);  // Create TSS key for thread-local buffer

    pthread_create(&thread1, NULL, thread_func, (void *)message1);
    pthread_create(&thread2, NULL, thread_func, (void *)message2);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Free buffers allocated by each thread (implementation detail omitted for brevity)

    tss_delete(buffer_key);

    return 0;
}

This example demonstrates how TSS can be used to allocate thread-specific buffers. Each thread retrieves its own buffer using tss_get, avoiding the need for complex memory management or global buffers.



    • Supported Compilers
      • GCC (requires __thread keyword)
      • Clang (with C11 or later support)
    • Mechanism
      • TLS is a language-specific feature that allows direct declaration of variables with thread-local storage.
      • These variables are allocated on the thread's stack and provide automatic thread safety.
    • Advantages
      • More concise and readable syntax than using tss_create and related functions.
      • Automatic cleanup of thread-local data when the thread exits.
    • Disadvantages
      • Not portable across all C compilers.
    • Example
    #include <pthread.h>
    
    __thread int thread_data;
    
    void *thread_func(void *arg) {
        thread_data = 42;  // Set thread-specific data
        printf("Thread %d: data = %d\n", (int)pthread_self(), thread_data);
        return NULL;
    }
    
    int main() {
        pthread_t thread;
        pthread_create(&thread, NULL, thread_func, NULL);
        pthread_join(thread, NULL);
        return 0;
    }
    
  1. Custom Data Structures with Mutexes

    • Mechanism
      • You can create your own data structures to hold thread-specific information.
      • Use mutexes to synchronize access to these data structures from different threads.
    • Advantages
      • More control over the data structure and its management.
    • Disadvantages
      • More complex to implement and maintain compared to tss_create or TLS.
      • Requires careful synchronization to avoid race conditions.
    • Example
    #include <pthread.h>
    
    typedef struct {
        int data;
        pthread_mutex_t mutex;
    } thread_data_t;
    
    thread_data_t thread_data_pool[MAX_THREADS];  // Pool for thread-specific data
    
    void initialize_thread_data(int index) {
        pthread_mutex_init(&thread_data_pool[index].mutex, NULL);
    }
    
    void *thread_func(void *arg) {
        int index = (int)arg;
        pthread_mutex_lock(&thread_data_pool[index].mutex);
        thread_data_pool[index].data = 42;  // Set thread-specific data
        printf("Thread %d: data = %d\n", (int)pthread_self(), thread_data_pool[index].data);
        pthread_mutex_unlock(&thread_data_pool[index].mutex);
        return NULL;
    }
    
    int main() {
        pthread_t threads[MAX_THREADS];
    
        // Initialize thread data pool
        for (int i = 0; i < MAX_THREADS; ++i) {
            initialize_thread_data(i);
        }
    
        for (int i = 0; i < MAX_THREADS; ++i) {
            pthread_create(&threads[i], NULL, thread_func, (void *)i);
        }
    
        for (int i = 0; i < MAX_THREADS; ++i) {
            pthread_join(threads[i], NULL);
        }
    
        // Cleanup (destroy mutexes)
    
        return 0;
    }
    

The choice between these alternatives depends on your specific needs and the C compiler you're using:

  • For more complex data structures or specific requirements, a custom approach with mutexes might be necessary. However, it requires careful implementation to ensure thread safety.
  • If portability is a major concern and TLS is not available, tss_create can be a good choice. Just remember to manage the keys properly to avoid memory leaks.
  • If your compiler supports TLS (GCC, Clang with C11 or later), it's generally the preferred option due to its simplicity and thread safety.