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
thread_local int counter;
Static
static thread_local int counter;
"C" style function
void foo()
{
static thread_local int counter;
}
Class method
class boo
{
static void foo()
{
static thread_local int counter;
}
};
Class data member
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
#pragma once
#include <unordered_map>
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:
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.