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.
Unlike local variables, static variables are not automatically thread confined.
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.
Thread local storage is static but it behaves quite differently from simple static storage.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With