Thread-local storage, union_cast et al.

L'API de thread-local storage de pthread est d'une simplicité remarquable (tout comme celle de Windows d'ailleurs). Une solution simple et élégante à un problème potentiellement complexe.

int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
int pthread_setspecific(pthread_key_t key, const void *value);
void *pthread_getspecific(pthread_key_t key);
int pthread_key_delete(pthread_key_t key);

Néanmoins, bien que l'interface soit simple à utiliser, elle détonne dans une application écrite en C++. L'utilisation de void pointeurs n'est effectivement pas une pratique courante n'y recommandée. De préférence, nous voudrions masquer détail derrière une interface plus respectueuse des types. Or, le C++ possède justement un mécanisme fait sur mesure pour cette tâche: les templates. Par ailleurs, à quoi bon demander au client de l'interface de générer une clé unique lorsque le langage possède déjà des objets qui sont, par définition, uniques.

Les classes ThreadLocal et ThreadLocalPointer

C'est ainsi qu'entrent en jeu les classes ThreadLocal et ThreadLocalPointer qui forment un wrapper autour de l'API de thread-local storage de pthread.

La classe ThreadLocal répond à un cas d'utilisation fréquent qui consiste à stocker un type primitif (int, bool, etc.) dans le thread-local storage. Dans cette situation, une pratique courante consiste à stocker directement la valeur du type primitif dans le pointeur passé en argument à la fonction pthread_setspecific.

Comme la technique ne fonctionne que pour les types dont la taille est inférieure ou égale à celle d'un pointeur, un static_assert est placé dans la classe ThreadLocal afin d'imposer explicitement cette limite.

template <typename T>
class ThreadLocal {
public:
    static_assert(sizeof(T) <= sizeof(void*),
            "ThreadLocal: sizeof(T) > sizeof(void*)");

    ThreadLocal() {
        pthread_key_create(&key, NULL);
    }

    ThreadLocal(const T value) {
        pthread_key_create(&key, NULL);
        set(value);
    }

    ~ThreadLocal() {
        pthread_key_delete(key);
    }

    void set(const T value) {
        pthread_setspecific(key, union_cast<void*>(value));
    }

    T get() const {
        return union_cast<T>(pthread_getspecific(key));
    }

private:
    // Disallow copy and assignment operators.
    ThreadLocal(const ThreadLocal<T>&);
    void operator=(const ThreadLocal<T>&);

    pthread_key_t key;
};

Pour stocker des données dont la taille dépasse celle d'un pointeur, il s'agit simplement de fournir à la méthode ThreadLocal::set(const T value) un pointeur en bonne et due forme. Celui-ci sera simplement converti en un void pointeur par le union_cast. Par exemple, dans le cas d'un pointeur vers un entier, nous construirions un objet dont la signature serait ThreadLocal<int*>.

La classe ThreadLocalPointer cache ce détail d'implantation tout en rendant ce cas d'utilisation plus explicite.

template <typename T>
class ThreadLocalPointer : public ThreadLocal<T*> {
public:
    ThreadLocalPointer() : ThreadLocal<T*>() {}

private:
    // Disallow copy and assignment operators.
    ThreadLocalPointer(const ThreadLocalPointer<T>&);
    void operator=(const ThreadLocalPointer<T>&);
};

Une note sur le union_cast

Il n'est pas possible d'utiliser, dans la méthode ThreadLocal::get, l'opérateur reinterpret_cast pour convertir le pointeur retourné par la fonction pthread_getspecific en entier. Il faut savoir que l'opérateur reinterpret_cast ne peut convertir un pointeur en un entier que si la taille de ce dernier est supérieure ou égale à celle du pointeur, c'est-à-dire qu'aucune troncation de la représentation binaire du pointeur ne peut être effectuée par l'opérateur reinterpret_cast.

Précisément, voici ce que le standard du C++ spécifie à propos de l'opérateur reinterpret_cast et les conversions entre pointeurs et entiers:

D'où l'utilisation d'une technique très répandue en C qui consiste à employer une union afin d'effectuer une conversion avec troncation entre deux types de tailles différentes. L'union_cast encapsule tout simplement cette technique dans une fonction générique dont l'invocation reprend la syntaxe d'un opérateur de casting.

template <class Dest, class Source>
inline Dest union_cast(Source source) {

    union {
        Source source;
        Dest dest;
    } converter;

    converter.source = source;
    return converter.dest;
}

D'autres approches sont envisageables, mais l'union_cast demeure sans doute la solution la moins cryptique. En outre, la plupart des compilateurs optimiseront sans problème le code additionnel composant l'union_cast.

Conclusion

Le code source complet du peut être consulté en ligne.