1

I've a private static method in a class that has all the methods as static. It's a helper class that helps with logging and other stuff. This helper class would be called by multiple threads. I don't understand how the method in question is working safely with multiple threads without a lock.

//Helper.cpp std::recursive_mutex Logger::logMutex_; void Logger::write(const std::string &logFilePath, const std::string &formattedLog) { std::lock_guard<std::mutex> guard(logMutex_); std::ofstream logFile(logFilePath.c_str(), std::ios::out | std::ios::app); if (logFile.is_open()) { logFile << formattedLog; logFile.close(); } } void Logger::error(const string &appId, const std::string &fmt, ...) { auto logFile = validateLogFile(appId); //Also a private static method that validates if a file exists for this app ID. if (!logFile.empty()) { //Format the log write(logFile, log); } } 
//Helper.h class Logger { public: static void error(const std::string &Id, const std::string &fmt, ...); private: static void write(const std::string &fileName, const std::string &formattedLog); static std::recursive_mutex logMutex_; }; 

I understand that the local variables inside the static methods are purely local. i.e., A stack is created every time these methods are called and the variables are initialized in them. Now, in the Logger::write method I'm opening a file and writing to it. So when multiple threads calls the write method through the Logger::error method(Which is again static) and when there's no lock, I believe I should see some data race/crash.
Because multiple threads are trying to open the same file. Even if the kernel allows to open a file multiple times, I must see some fault in the data written to the file.

I tested this with running up to 100 threads and I see no crash, all the data is being written to the file concurrently. I can't completely understand how this is working. With or without the lock, I see the data is being written to the file perfectly.

TEST_F(GivenALogger, WhenLoggerMethodsAreCalledFromMultipleThreads_AllTheLogsMustBeLogged) { std::vector<std::thread> threads; int num_threads = 100; int i = 0; for (; i < num_threads / 2; i++) { threads.push_back(std::thread(&Logger::error, validId, "Trial %d", i)); } for (; i < num_threads; i++) { threads.push_back(std::thread(&Logger::debug, validId, "Trial %d", i)); } std::for_each(threads.begin(), threads.end(), [](std::thread &t) { t.join(); }); auto actualLog = getActualLog(); // Returns a vector of log lines. EXPECT_EQ(num_threads, actualLog.size()); } 

Also, how should I properly/safely access the file?

3
  • First: What are you talking about "without the lock"? As shown, the only time error does anything with the underlying file is through its own call to write, which means that every write in your code actually is protected by locking logMutex_. It's possible that there's some undefined behavior due to validateLogFile, but you haven't shown the contents of that function. Second: Undefined behavior is undefined. "I believe I should see a crash" is unfounded. Commented Jan 18, 2023 at 6:11
  • @NathanPierson 1. What I meant by without the lock is when I remove the lock, it still works fine. 2. validateLogFile just verifies if the file exists in the directory and returns the filePath if exists, if not an empty string. Commented Jan 18, 2023 at 6:28
  • Not related : But from a design point of view, consider giving your logger an abstract baseclass (interface) and inject that into the classes that want to log. That will get rid of all your static functions and will make other classes more testable (it also means the logger can protect itself wrt multithreading and keep the file open for as long as it lives). For unit testing other classes you can then inject a logger implementation that does nothing. Commented Jan 18, 2023 at 6:48

1 Answer 1

4

The key is this line:

std::lock_guard<std::mutex> guard(logMutex_); 

The std::lock_guard<std::mutex> will lock the mutex logMutex_ in the constructor and will unlock the mutex in the destructor when it goes out of scope, ie when the method returns.

If another thread attempts to write while the first thread is within the guard scope, the new (local) guard will try to lock the logMutex_ and that thread will be put to sleep until the lock is released.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.