Most data in a C++ program is either shared by all threads or private to a single function call. Global and static data members are shared; automatic data members are placed on a thread's stack and are visible only to the function that creates them (and functions to which it is called).
A third possibility exists: data that have global scope and duration but are private to each thread; each thread is given a separate object. The feature is called thread local storage (TLS). In standard C++, the keyword for this is thread_local.
Thread local data are useful for pieces of data which a thread must retain between different function calls, which are still private to only the thread itself. Examples: caches, error/context objects, random number generators, counters which would otherwise need protection using a mutex.
To give a variable thread storage duration, prefix the declaration with thread_local. This may precede a global variable, a namespace scope declaration, or a static variable inside a function or class.
Every thread which accesses the variable will have its own separate instance.
#include <iostream>
#include <thread>
#include <vector>
thread_local int counter = 0;
void increment_and_print(int id)
{
for (int i = 0; i < 3; ++i)
++counter;
std::cout << "Thread " << id
<< " counter = " << counter << '\n';
}
int main()
{
// Start this function on a separate thread
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i)
threads.emplace_back(increment_and_print, i);
for (auto& t : threads)
t.join();
std::cout << "Main thread counter = "
<< counter << '\n';
return 0;
}
A thread local counter globally is declared; each of the four secondary threads is given its own private copy initialized to 0; thus when increment_and_print is called within one of them only one instance is modified. The value that will be printed by each of the four threads is therefore independent of the other threads, as is the value that would be printed by main if called instead.
Thread local variables are initialized when first accessed by any particular thread. Static global variables are initialized when execution leaves static initialization and begins execution at main, before threads are created but after static storage for programs is set up for the main thread.
If dynamic initialization requires a constructor, for instance, then a thread local variable will be constructed on first odr use.
When the thread terminates, any thread local objects declared with that thread are destructed (with destructors called in the reverse order to their construction); still alive while the thread exits. That is, thread local objects and variables are destructed after any automatic data left on the stack and before the thread's resources are reaped by the OS.
While automatic objects are constructed and destructed once for each call to a function, or every time there is a call to code that is itself recursive, thread local data items will not be constructed (or destructed) until the thread that uses them is initiated and it is called the first time, respectively.
thread_local can appear in front of a static function local variable. That variable will still have thread storage duration, but it will also only be visible within that function.
int next_id()
{
thread_local int id = 0;
// Increment the value before the expression uses it
return ++id;
}
Each thread calling next_id() gets a new sequence of values: the values obtained will never collide with the values obtainable by another thread, even if many are generated, without an intervening critical section. That means its value is kept up to date throughout the lifetime of its thread, and continues where it left off between successive calls.
Any class that can have static instances can now also have thread local instances.
#include <iostream>
#include <thread>
#include <string>
class Logger {
public:
Logger()
{
std::cout << "Logger constructed for a thread\n";
}
~Logger()
{
std::cout << "Logger destroyed for a thread\n";
}
void log(const std::string& msg)
{
std::cout << msg << '\n';
}
};
thread_local Logger logger;
void worker(int id)
{
logger.log("Message from thread " + std::to_string(id));
}
int main()
{
// Declare t1 before code needs to call it
std::thread t1(worker, 1);
std::thread t2(worker, 2);
t1.join();
t2.join();
return 0;
}
Each of the two secondary threads gets its own instance of a Logger class. The object is constructed when that thread first accesses logger and when the thread terminates, this instance will be destructed.
Main will never touch logger, will never construct an instance, and the program will print "Logger constructed..." and "Logger destroyed..." twice.
Thread local storage does remove data races for data that are thread local since we are just replacing one access to shared data by one access to each thread's private copy of that data. It does not magically transform non thread safe code into thread safe.
Passing thread local data around; if a pointer/reference is passed to another thread that will access it, there are still data race, or other, issues. Passing the address of thread local data to any non thread that will still access it is almost always a bad idea!
Caches that would otherwise require a mutex protected shared cache.
Error state or context objects: these variables need to survive call frames in some thread, but not at all in others.
The source of data for a random number generator, which
requiresthat its state be saved.Non reentrant functions which maintain state and require a separate copy of that state when called by multiple threads (
i.e. Non reentrant functions that the application cannot change).
Thread local storage should only be used to prevent other threads from interfering with that specific object, rather than creating some kind of global array.
The cost of non trivial constructors and destructors can be prohibitive. Consider other approaches for performance critical code that need thread local data: possibly lazy initialization.
The lifetime of a thread local object is guaranteed. Any thread local variable is destructed after all automatic variables, and before the thread exits itself.
Don't worry about the compiler specific thread storage class specifiers
__thread,__declspec(thread), etc. These are not standard C++;thread_localis the only thing necessary in modern C++.
0 comments
No comments yet. Be the first.