I am writing a logger and would like to make it thread-safe. I have done so by doing the following:
class Logger
{
public:
virtual ~Logger();
LogSeverity GetSeverity() const;
void SetSeverity(LogSeverity s);
protected:
std::mutex mutex;
private:
LogSeverity severity;
};
void Logger::SetSeverity(LogSeverity s)
{
std::lock_guard<std::mutex> lock(mutex);
severity = s;
}
LogSeverity Logger::GetSeverity() const
{
std::lock_guard<std::mutex> lock(mutex);
return severity;
}
void Logger::SetSeverity(LogSeverity s) const
{
std::lock_guard<std::mutex> lock(mutex);
severity = s;
}
// StreamLogger inherits from Logger
void StreamLogger::SetStream(ostream* s)
{
std::lock_guard<std::mutex> lock(mutex);
stream = s;
}
ostream* StreamLogger::GetStream() const
{
std::lock_guard<std::mutex> lock(mutex);
return stream;
}
However, all public access to the class require this extremely redundant lock.
Two options I see:
1) Caller of these public function will lock the whole object using the mutex within the class
Logger l = new Logger();
std::lock_guard<std::mutex> lock(l->lock());
l->SetSeverity(LogDebug);
2) Wrapper lock around each variable in the class
template typename<T> struct synchronized
{
public:
synchronized=(const T &val);
// etc..
private:
std::mutex lock;
T v;
};
class Logger
{
private:
synchronized<LogSeverity> severity;
};
However, this solution is very resource intensive, lock for each item.
Am I on the right track or is there something I am missing?
First of all, you need to carefully reconsider possible use cases:
I have a strange feeling, that you think about your classes in a very small perspective: "well, it's a logger, so I will put all possibly useful features into it" (I may be wrong). Classes should have complete, but minimal interfaces, explicitly representing what particular class is responsible for. Think about that.
As for your multi-threading problem: I don't think shared logger is a good idea. Personally, I always prefer thread-specific primitives in such cases (one logger per thread). Why?
If your compiler supports C++ 11, above solution is basically proper usage of thread_local, __declspec(thread) or __thread, depending on what your compiler supports.
If you still want to implement shared logger, start from design review. For example: are you sure, that changing single property requires locking a mutex? Things like severity member is perfect candidate for std::atomic. It may require more work, but can be much faster.
class Logger
{
//cut
private:
std::atomic<LogSeverity> severity;
};
void Logger::SetSeverity(LogSeverity s)
{
severity.store(s, std::memory_order_release);
}
LogSeverity Logger::GetSeverity() const
{
return severity.load(std::memory_order_acquire);
}
std::memory_order_acquire/release is just an example - you may want to use stronger ordering like memory_order_seq_cst (if you need total global ordering). However acquire/release pair is usually enough to ensure proper synchronization between loads and stores and small bonus - they will not produce any fences on x86.
If think you might want to read C++ Concurrency in Action by Anthony Williams. It is the best resource for learning threads, atomics, synchronization, memory orderings and more.
There is also a very good articles on Bartosz Milewski's blog. Like this one: C++ atomics and memory ordering .
If you are not familiar with topics like atomics, fences, orderings etc., these resources are very good to start with.
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