1
\$\begingroup\$

I'm new to C++ and am following up on: A simple multithreaded FileLogger in C++

I am curious to know if I implemented his ideas correctly, if there are further improvements to the code and whether it's (still) correct.

FileLoggerThread.h

#pragma once
#include <string>
#include <fstream>
#include <mutex>
#include <deque>
#include <condition_variable>
#include <filesystem>

class FileLoggerThread
{
public:
    explicit FileLoggerThread(std::filesystem::path filePath);
    ~FileLoggerThread();                            // makes code more easily unit testable (unit tests can otherwise be in an infinite loop)

    void log(const std::string &msg);

    FileLoggerThread(const FileLoggerThread&) = delete;
    FileLoggerThread& operator=(const FileLoggerThread&) = delete;

private:
    void writeToFile();

    std::deque<std::string> mMessages;              // accessed by log (any thread) and writeToFile (the writer thread)
    std::ofstream mOutputFile;                      // only accessible by writeToFile
    
    // thread internal resources
    std::mutex mMessagesMutex;                      // lock/unlock this each time messages is pushed or popped
    std::condition_variable mThreadScheduler;       // log needs to wake up writeToFile
    bool mIsRunning;                                // if set to false then the writer thread stops
};

FileLoggerThread.cpp

#include "FileLoggerThread.h"
#include <thread>

FileLoggerThread::FileLoggerThread(std::filesystem::path filePath) : mOutputFile {filePath}, mIsRunning {true} {
    std::thread t(&FileLoggerThread::writeToFile, this);
    t.detach();
}

void FileLoggerThread::writeToFile() {
    while (mIsRunning) {
        std::deque<std::string> localMessages;
        std::unique_lock<std::mutex> writerLock {mMessagesMutex};
        mThreadScheduler.wait(writerLock, [&]{ return !mMessages.empty() || !mIsRunning;});
        localMessages.swap(mMessages);
        writerLock.unlock();

        for (const auto& message : localMessages) {
            mOutputFile << message << std::endl;
        }
    }
}

void FileLoggerThread::log(const std::string &msg) {
    std::unique_lock loggerLock(mMessagesMutex);
    mMessages.emplace_back(msg);
    loggerLock.unlock();
    mThreadScheduler.notify_one(); // done for performance reasons, see: https://youtu.be/F6Ipn7gCOsY?t=2205
}

FileLoggerThread::~FileLoggerThread() {
    mIsRunning = false;
    mThreadScheduler.notify_one();
}
\$\endgroup\$
3
  • \$\begingroup\$ "and whether its (still) correct" Did you test it? Does it appear to be correct? \$\endgroup\$
    – Mast
    Commented Feb 17, 2021 at 16:57
  • \$\begingroup\$ Yes, I tested it, it appears to be correct. I should've made that explicit, perhaps, I somehow thought that my own personal testing was implied. \$\endgroup\$ Commented Feb 18, 2021 at 8:26
  • \$\begingroup\$ You'd hope so, but experience tells us that isn't always the case. Thank you for clarifying. \$\endgroup\$
    – Mast
    Commented Feb 18, 2021 at 11:45

1 Answer 1

1
\$\begingroup\$
  • It is not a good idea to have a completely detached thread. When one detaches thread, they usually use some sort of notification system like future/promise to ensure that the thread finished using resources. Here you file logger might have finished working and got destroyed while the detached thread still accesses this leading to UB. Instead you should keep thread as a part of the logger and trigger join in the destructor.

  • You shouldn't use std::endl; for every message - the flushing is a slow operation. You surely don't have a good reason to trigger it more often than more than once a second . Just make sure that you do trigger it once a second or so. std::condition_variable::wait_for/wait_until could be used to ensure that.

  • Another bug in destructor is that you modify mIsRunning without locking the mutex. Technically, it is a UB. In practice, this bug results in some very rare cases with the wait missing the notification and lasting eternity.

  • With the scheme that you are running I don't see why you use a std::deque for holding messages. Why not just std::vector? It is more efficient and also you shouldn't create new localMessages each time - just keep one constantly existing and clear it after each use. It saves some unnecessary memory allocations.

  • Also, once mIsRunning is false you need to ensure that all messages were written instead of quitting. Moreover, you shouldn't access mIsRunning in the while-loop condition as it should be guarded by the mutex.

  • The log function void log(const std::string &msg); accepts a string and copies it into the queue. It is a waste. You'd better have two other overloads: void log(std::string&& msg); and void log(std::string_view msg); with the former moving the string and it's content for printing instead of unnecessarily copying it, while latter is just a more general form of const std::string&.

  • In general, logger should support features like verbosity filterting and identification (e.g., info, warning, error), timestamping, and similar stuff that should be inherent part of a logger. Here you just accept arbitrary messages.

\$\endgroup\$
4
  • \$\begingroup\$ Is it standard practice to join the thread in the destructor? If so, then the mIsRunning bool is not needed is it? You can just create an infinite loop and join the thread in the destructor. Or is that an unsafe design? \$\endgroup\$ Commented Feb 18, 2021 at 10:09
  • \$\begingroup\$ @Melvin infinite loops are only used when programming microcontrollers. In all other cases you want an exit condition. \$\endgroup\$
    – Mast
    Commented Feb 18, 2021 at 11:46
  • \$\begingroup\$ Fair enough @Mast, fair enough :) \$\endgroup\$ Commented Feb 18, 2021 at 11:52
  • 1
    \$\begingroup\$ @MelvinRoest join simply waits till the thread exists. You need some other scheme that ensures thread's exit. So use mIsRunning or something similar is needed. \$\endgroup\$
    – ALX23z
    Commented Feb 18, 2021 at 15:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.