Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can a C++11 thread_local variable inherit its initial value from the parent thread?

I'd like to have a thread_local variable to change the level of logging applied in each thread of my application. Something like so:

enum class trace_level { none, error, warning, log, debug, verbose };
static thread_local trace_level min_level = trace_level::log;

The default value should be trace_level::log for the main thread when the application starts, but if it is changed before launching other threads, then I would like the child threads to start with the current value of the parent.

Is there any way to do this using a thread_local variable? Since this code is buried in a library it is not an option to simply set the value manually at the start of each thread.

like image 700
marack Avatar asked May 09 '14 02:05

marack


People also ask

Are static variables thread local?

Unlike local variables, static variables are not automatically thread confined.

Why use thread_ local?

thread_local is used to mark a variable with thread storage duration, which means it is created when the thread starts and cleaned up when the thread ends. Note: For C++, C++11 or later is required to use the thread_local keyword.

Is Thread_local always static?

Thread local storage is static but it behaves quite differently from simple static storage.

How does thread_ local work?

In C++, thread_local is defined as a specifier to define the thread-local data and this data is created when the thread is created and destroyed when the thread is also destroyed, hence this thread-local data is known as thread-local storage.


2 Answers

This already happens if the initialization is dynamic. The standard requires that variables with "thread storage duration" and dynamic initialization be initialized sometime between the start of the thread and the 'first odr-use'. However, since you generally can't control exactly when that initialization will occur (other than sometime after the thread object is created and sometime before the thread ends - assuming the thread local variable actually gets used by the thread) the problem is that the thread local variable might get initialized with a value that your main thread sets after the thread is created.

For a concrete example, consider:

#include <stdio.h>

#include <chrono>
#include <functional>
#include <thread>
#include <string>

using std::string;

enum class trace_level { none, error, warning, log, debug, verbose };

trace_level log_level = trace_level::log;


static thread_local trace_level min_level = log_level;
void f(string const& s)
{

    printf("%s, min_level == %d\n", s.c_str(), (int) min_level);
}



int main()
{
    std::thread t1{std::bind(f,"thread 1")};

    //TODO: std::this_thread::sleep_for(std::chrono::milliseconds(50));

    log_level = trace_level::verbose;
    std::thread t2{std::bind(f,"thread 2")};

    t1.join();
    t2.join();
}

With the sleep_for() call commented out as above, I get the following output (usually):

C:\so-test>test
thread 1, min_level  == 5
thread 2, min_level  == 5

However, with the sleep_for() uncommented, I get (again - usually):

C:\so-test>test
thread 1, min_level  == 3
thread 2, min_level  == 5

So as long as you're willing to live with a bit of uncertainty regarding which logging level a thread will get if the level gets changed in the main thread soon after the thread starts, you can probably just do what you're looking to do pretty naturally.

There's one remaining caveat - data races. The code above has a data race on the log_level variable, so it actually has undefined behavior. The fix for that is to make the variable either an atomic type or wrap it in a class that uses a mutex to protect updates and reads from data races. So change the declaration of the global log_level to:

std::atomic<trace_level> log_level(trace_level::log);

Standards citations:

3.6.2 Initialization of non-local variables [basic.start.init]

... Non-local variables with thread storage duration are initialized as a consequence of thread execution. ...

and

3.7.2/2 Thread storage duration [basic.stc.thread]

A variable with thread storage duration shall be initialized before its first odr-use (3.2) and, if constructed, shall be destroyed on thread exit.

like image 189
Michael Burr Avatar answered Sep 24 '22 15:09

Michael Burr


You can create a global pointer to a parent thread local variable.

In global scope

thread_local trace_level min_level = trace_level::log;
trace_level *min_level_ptr = nullptr;

Then, in each thread you can do:

if (!min_level_ptr)
    min_level_ptr = &min_level;
else
    min_level = *min_level_ptr;

(Possibly, make the min_level_ptr atomic for added safety and use atomic compare exchange instead of assignment).

The idea goes as following: each thread's local storage occupies a different region in memory, so min_level variable in one thread has unique storage address different from all other. min_level_ptr, on the other hand, has the same address, no matter which thread is accessing it. As "parent" thread starts before all other, it will claim the globally shared pointer with its own min_level address. The children will then initialize their values from that location.

like image 26
oakad Avatar answered Sep 22 '22 15:09

oakad