My logger posted below makes use of condition variables in order to notify the "flusher" thread that new items are available. The notification occurs every time a message is logged.
My questions are:
- If the notification immediately follows the log action then does that mean that the queue will only contain one element at most?
What would be the difference between:
{ std::unique_lock<std::mutex> lock(mMutex); mLogItems.push_back(std::make_pair(inLevel, std::move(inMessage))); mCondition.notify_one(); }
and
{ std::unique_lock<std::mutex> lock(mMutex); mLogItems.push_back(std::make_pair(inLevel, std::move(inMessage))); } mCondition.notify_one();
And which is correct?
Should I prefer an approach where flushing occurs at periodic intervals?
If yes, then should I use a loop where I flush and sleep until a stop condition? Or should
sleep()
be avoided?Is it feasible to use a lockless approach?
Working code sample:
#include <atomic>
#include <condition_variable>
#include <deque>
#include <iostream>
#include <mutex>
#include <sstream>
#include <thread>
enum class LogLevel
{
Debug,
Info,
Warning,
Error
};
const char * ToString(LogLevel inLogLevel)
{
switch (inLogLevel)
{
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARNING";
case LogLevel::Error: return "ERROR";
default: return "UNKNOWN";
}
}
class Logger
{
public:
static Logger & Instance()
{
static Logger fInstance;
return fInstance;
}
~Logger()
{
setQuit();
}
void setQuit()
{
std::unique_lock<std::mutex> lock(mMutex);
mQuit = true;
mCondition.notify_one();
}
void setTreshold(LogLevel inLogLevel)
{
mTreshold = inLogLevel;
}
LogLevel getTreshold() const
{
return mTreshold;
}
bool isAllowed(LogLevel inLogLevel) const
{
bool result = inLogLevel >= mTreshold;
return result;
}
typedef std::pair<LogLevel, std::string> LogItem;
typedef std::deque<LogItem> LogItems;
void log(LogLevel inLevel, std::string inMessage)
{
if (mQuit)
{
return;
}
{
std::unique_lock<std::mutex> lock(mMutex);
mLogItems.push_back(std::make_pair(inLevel, std::move(inMessage)));
mCondition.notify_one();
}
}
template<typename PrintFunction>
void flush(const PrintFunction & inPrintFunction)
{
while (!mQuit)
{
LogItems items;
{
std::unique_lock<std::mutex> lock(mMutex);
if (mLogItems.empty())
{
mCondition.wait(lock); // unlocks the mutex
}
items.swap(mLogItems); // internal pointer swap
}
for (LogItems::const_iterator it = items.begin(), end = items.end(); it != end; ++it)
{
inPrintFunction(*it);
}
}
}
private:
std::atomic<bool> mQuit;
LogLevel mTreshold;
std::mutex mMutex;
std::condition_variable mCondition;
LogItems mLogItems;
};
struct LogHelper
{
LogHelper(LogLevel inLevel) :
mLevel(inLevel)
{
}
~LogHelper()
{
Logger::Instance().log(mLevel, mStream.str());
}
std::stringstream & ss()
{
return mStream;
}
private:
LogLevel mLevel;
std::stringstream mStream;
};
#define LOG(LEVEL) \
if (!Logger::Instance().isAllowed(LogLevel::LEVEL)) {} else LogHelper(LogLevel::LEVEL).ss()
#define LogInfo() LOG(Info)
#define LogWarning() LOG(Warning)
#define LogError() LOG(Error)
#define LogDebug() \
if (!Logger::Instance().isAllowed(LogLevel::Debug)) {} else LogHelper(LogLevel::Debug).ss() << __FILE__ << ":" << __LINE__ << ": "
void Thread(const std::string & inName)
{
for (unsigned i = 0; i < 5; ++i)
{
LOG(Info) << inName << " " << i;
}
}
struct print_to_ostream
{
print_to_ostream(std::ostream & os) : os(os) {}
void operator()(const Logger::LogItem & inLogItem) const
{
os << ToString(inLogItem.first) << "\t" << inLogItem.second << std::endl;
}
std::ostream & os;
};
int main()
{
LogDebug() << "Start of program!";
LogInfo() << "Test info";
LogWarning() << "Test warning";
LogError() << "Test error";
// Start a flush thread
std::thread flusher([]{ Logger::Instance().flush(print_to_ostream(std::cout)); });
LogDebug() << "Starting threads...";
std::thread t1(std::bind(&Thread, "t1"));
std::thread t2(std::bind(&Thread, "t2"));
std::thread t3(std::bind(&Thread, "t3"));
std::thread t4(std::bind(&Thread, "t4"));
std::thread t5(std::bind(&Thread, "t5"));
LogDebug() << "Join threads...";
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
// Cleanup
LogDebug() << "Cleanup ...";
Logger::Instance().setQuit();
flusher.join();
}
mCondition.notify_one();
flush, or merely send a message to another thread? \$\endgroup\$