Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top