Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++11

Multi Instance Thread Local Storage

4.75/5 (3 votes)
17 Feb 2015CPOL3 min read 17.5K   95  
MI TLS(Multi Instance Thread Local Storage) is generalization of Thread Local Storage, introduced in C++11.

Introduction

Starting from C++11, Thread Local Storage became part of C++ language. The concept itself is not new, and many C++ compilers had compiler specific TLS. Here, you may find a very good explanation about different storage types in common, and particularly about TLS.

Standard C++11 TLS

C++11 thread_local storage specifier gives couple of options to declare TLS variable.

Global

C++
thread_local int counter;

Static

C++
static thread_local int counter; 

"C" style function

C++
void foo()
{
    static thread_local int counter;
}

Class method

C++
class boo
{
    static void foo()
    {
        static thread_local int counter;
    }
};

Class data member

C++
class boo
{
    static thread_local int counter;
};

In all listed above cases, model of having one instance per thread exactly matches our expectations.

But what if we would like to have one instance of TLS data, per thread and per object instance?

In the last snippet, if one thread will access multiple instances of class boo objects, all instances will use single counter TLS.

Multi Instance TLS will provide such functionality - objects instances of the same class, if they have TLS data member, will have separate copies of that data member.

Implementation

C++
#pragma once

#include <unordered_map>

// Forward declaration
template <typename T> class mi_tls_repository;

template < typename T> class mi_tls : protected mi_tls_repository<T>
{
public:
    mi_tls<T>() {}

    mi_tls<T>(const T& value)
    {
        this->store(reinterpret_cast<uintptr_t>(this), value);
    }

    mi_tls<T>& operator = (const T& value)
    {
        this->store(reinterpret_cast<uintptr_t>(this), value);
        return *this;
    }

    operator T()
    {
        return this->load(reinterpret_cast<uintptr_t>(this));
    }

    ~mi_tls()
    {
        this->remove(reinterpret_cast<uintptr_t>(this));
    }
};

template <typename T> class mi_tls_repository
{
protected:
    void store(uintptr_t instance, T value)
    {
        repository[instance] = value;
    }

    T load(uintptr_t instance)
    {
        return repository[instance];
    }

    void remove(uintptr_t instance)
    {
        repository.erase(instance);
    }

private:
    static thread_local std::unordered_map<uintptr_t, T> repository;
};

template <typename T> thread_local std::unordered_map<uintptr_t, T> mi_tls_repository<T>::repository;

The implementation is so simple, that any "inside" comments will just "pollute". I just will explain the idea.

We cannot use thread_local storage in the class because, as we saw, same TLS variable will be shared between different object instances of the same class that are accessible from one thread. Instead of that, we will have repository which will store TLS data per object instance, when key in this repository will be an object address. And repository object itself will be standard C++11 TLS.

To make usage of mi_tls user friendly, and to have access methods exactly like access to the native type, we need Constructor and Assign Operator that will accept native type, and Cast Operator will know to make "seamless" casting to the native type.

So, if we defined:

mi_tls<int> counter;

we may use counter like a native integer.

When object, that has mi_tls as a data member constructed, mi_tls instance will be automatically registered in the repository. When object is destructed, mi_tls will be automatically unregistered.

Multi Instance TLS Use Case Example

Let's look at Multiple Readers Single Writer lock. Most implementations (like this) have one defect - any thread may unlock Reader Lock, even if that specific thread didn't take this lock before. The way to fix it is to have some bookkeeping, per thread, and per Lock instance, some counter, that will indicate how many times thread took the lock (let's assume that reader lock is re-enterable). In this case, if thread tries to release Reader Lock, but counter is zero, we just will ignore this call.

Looks like the native way to implement that is to use TLS for the counter. But if we will use standard C++11 TLS, that will not work as expected. All locks that are accessible from a particular thread will share one single counter. But as we mentioned earlier, we want "to have some bookkeeping, per thread, and per Lock instance", and here MI TLS is exactly what we need.

Complexity

Complexity of MI TLS is derived from the complexity std::unordered_map - Constant in average, linear in worst (and very rear) case. If MI TLS is used for Hard Real Time Systems, using std::map instead of std::unordered_map may be a good option. std::map is actually implemented as a balanced tree and has O(log n) average and worst complexity.

Thread Safety and Synchronization Complexity

Problems with a Thread Safety come only when we have shared data that is accessible from multiple threads. In MI TLS implementation, the only data that we have is:

C++
static thread_local std::unordered_map<uintptr_t, T> repository;

and as we saw this repository has Standard C++11 thread_local storage specifier. Which means that every thread has his own copy of the repository. So, MI TLS is absolutely Thread Safe and doesn't require any synchronisation.

ENJOY USING MI TLS.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)