Professional C__ - Marc Gregoire [400]
return;
}
// Start processing loop.
unique_lock while (true) { // Wait for a notification. mCondVar.wait(lock); // Condition variable is notified, so something is in the queue // and/or we need to shut down this thread. lock.unlock(); while (true) { lock.lock(); if (mQueue.empty()) { break; } else { ofs << mQueue.front() << endl; mQueue.pop(); } lock.unlock(); } if (mExit) break; } } Code snippet from logger\Version2\Logger.cpp A second problem with the first naïve implementation is that sometimes the program might just block indefinitely in a deadlock situation where the main thread is blocking on the following line in the Logger destructor: mThread.join(); And the Logger background thread is blocking on the following line in the processEntries() method: mCondVar.wait(lock); That is because the code contains a race condition. The main thread launches the processEntries() background thread in the Logger constructor, but immediately continues executing the remaining code in the main() function. This remaining code is rather small in this example. It can happen that this remaining code from the main() function, including the Logger destructor, is executed before the Logger background thread has started its processing loop. When that happens, the Logger destructor will already have called notify_all() before the background thread is waiting for the notification, and thus the background thread will miss this notification from the destructor. To solve this race condition, the Logger constructor should wait after launching the background thread until the background thread is ready to process entries. You can implement this by adding a Boolean variable (mThreadStarted), a mutex (mMutexStarted), and a condition variable (mCondVarStarted) to the Logger class. Here is the final definition: class Logger { public: // Starts a background thread writing log entries // to a file. Logger(); // Gracefully shut down background thread. virtual ~Logger(); // Add log entry to the queue. void log(const std::string& entry); protected: void processEntries(); bool mThreadStarted; bool mExit; // Mutex and condition variable to protect access to the queue. std::mutex mMutex; std::condition_variable mCondVar; std::queue // The background thread. std::thread mThread; // Mutex and condition variable to detect when the thread // starts executing its loop. std::mutex mMutexStarted; std::condition_variable mCondVarStarted; private: // Prevent copy construction and assignment. Logger(const Logger& src); Logger& operator=(const Logger& rhs); }; Code snippet from logger\FinalVersion\Logger.h The final implementation of the Logger class is as follows. Note that the constructor is using the wait() method accepting a predicate. Logger::Logger() : mThreadStarted(false), mExit(false) { // Start background thread. mThread = thread{&Logger::processEntries, this}; // Wait until background thread starts its processing loop. unique_lock mCondVarStarted.wait(lock, [&](){return mThreadStarted == true;}); } Logger::~Logger() { // Gracefully shut down the thread by setting mExit // to true and notifying the thread. mExit = true; // Notify condition variable to wake up thread. mCondVar.notify_all(); // Wait until thread is shut down. mThread.join(); } void Logger::log(const std::string& entry) { // Lock mutex and add entry to the queue. unique_lock mQueue.push(entry); // Notify condition variable to wake up thread. mCondVar.notify_all(); } void Logger::processEntries() { // Open log file. ofstream ofs("log.txt"); if (ofs.fail()) { cerr << "Failed to open logfile." << endl; return; } // Start processing loop. unique_lock // Notify listeners that thread is starting processing loop. mThreadStarted = true; mCondVarStarted.notify_all(); while (true) { // Wait for a notification. mCondVar.wait(lock); // Condition variable is notified, so something is