Search code examples
c++multithreadingsynchronizationrace-conditioncritical-section

How could I make the access of commonly shared object thread-safe?


I have searched through the entire SO forum but couldn't find any plausible answers. I know resource sharing can be made threadsafe by using synchronisation mechanisms like mutexes and semaphores but my question is different. Let's say you have a Logger logger object. Which is passed by reference to all the running threads. This logger object has a Logger::log function where are a resource (log file) is accessed by multiple threads. I can wrap this critical section with mutexes with the method in order to prevent race conditions. But how can I prevent the race condition which might occur when multiple threads access the shared Logger logger object. I could pass a common mutex lock along with the Logger logger object and try to acquire the object before using it but that solution is not elegant. Whats the best way to prevent race condition in this given scenario?


Solution

  • More in sync with Single Responsibility Principle and C++ idea of not to pay for what you dont use is to have Logger class responsible only for logging. Imagine that you want to reuse it for logging in single threaded application - then why to pay for extra synchronization stuff.

    But of course for multi-threaded application - you will need synchronization mechanism between threads. One good way to achieve this is to use Decorator Design Pattern - like below:

    class ILogger 
    {
    public: 
       virtual ~ILogger() = default;
    
       // as many log methods as you need
       virtual void log(args) = 0;
    };
    

    Then make your Logger deriving from and implementing ILogger.

    Then define decorator that adds synchronization to it:

    class SynchronizedLogger : public ILogger 
    {
    public: 
       SynchronizedLogger (ILogger& logger) : logger(logger) {} 
    
       // as many log methods as you need
       void log(args) override
       {
           std::lock_guard<std::mutex> lock(logGuard);
           logger.log(args);
       }
    
    private:
       std::mutex logGuard;
       ILogger& logger; 
    };
    

    You only have to ensure that all threads that will use logging starts after construction of SynchronizedLogger and stops before its destruction:

    int main() {
       Logger logger{...}; 
       SynchronizedLogger syncLogger{logger};
    
       std::thread t1([&syncLogger] { syncLogger.log("T1"); });
       std::thread t2([&syncLogger] { syncLogger.log("T2"); });
       std::thread t3([&syncLogger] { syncLogger.log("T3"); });
       std::thread t4([&syncLogger] { syncLogger.log("T4"); });
    
      t1.join();
      t2.join();
      t3.join();
      t4.join();
    }