【问题标题】:Adding Thread Safety to a Simple Logging Function?将线程安全添加到简单的日志记录功能?
【发布时间】:2012-03-25 17:48:02
【问题描述】:

根据我的阅读,standard output streams are generally not thread safe。我有一个 C++ 应用程序(基于 Windows,使用 Visual Studio 2005),它有一个非常简单的日志记录功能:

void logText(string text)
{
    if(g_OutputLogEnabled && g_OutputLog.is_open())
    {
        string logDate = getDateStamp("%Y-%m-%d %H:%M:%S");
        g_OutputLog << "[" << logDate << "]: " << text << endl;
    }

    cout << text << endl; // Also echo on stdout
}

在本例中,g_OutputLog 是一个 ofstream,g_OutputLogEnabled 是一个布尔值。

我一直在我的主应用程序中使用这个小函数没有问题,但我现在想将它的使用扩展到一些子线程。这些线程会工作并在工作完成时异步打印数据。

问题:如何向这个例程添加简单的行级线程安全?我真正关心的是进入的每一行在我的日志中保持不变。在这种情况下,性能不是问题,但(一如既往)更快更好。

我知道我可以使用第三方日志记录包,但我想自己做,以便了解它的工作原理。我的多线程知识不是应有的水平,我正在努力提高这一点。

我听说过critical sections 一词,并且我对互斥锁和信号量有所了解,但在这种情况下我会使用哪个?有没有干净、简单的解决方案?提前感谢您的任何建议。

【问题讨论】:

    标签: c++ windows multithreading thread-safety iostream


    【解决方案1】:

    使用范围锁,例如:

    void logText(string text)
    {
        if(g_OutputLogEnabled && g_OutputLog.is_open())
        {
            string logDate = getDateStamp("%Y-%m-%d %H:%M:%S");
    
            boost::scoped_lock (g_log_mutex);  //lock
            g_OutputLog << "[" << logDate << "]: " << text << endl;
    
        } //mutex is released automatically here
        
        boost::scoped_lock (g_cout_log_mutex); //lock on different mutex!
        cout << text << endl; // Also echo on stdout
    }
    

    如果您的编译器支持,您也可以使用std::unique_lock


    如果您不能使用 Boost 并且没有 std::unique_lock,您将如何实现 scoped_lock

    首先定义mutex类:

    #include <Windows.h>
    
    class mutex : private CRITICAL_SECTION  //inherit privately!
    {
    public:
         mutex() 
         {
            ::InitializeCriticalSection(this);
         }
         ~mutex() 
         {
            ::DeleteCriticalSection(this);
         }
    private:
    
         friend class scoped_lock;  //make scoped_lock a friend of mutex
    
         //disable copy-semantic 
         mutex(mutex const &);           //do not define it!
         void operator=(mutex const &);  //do not define it!
    
         void lock() 
         {
            ::EnterCriticalSection(this);
         }
         void unlock() 
         {
            ::LeaveCriticalSection(this);
         }
    };
    

    然后将scoped_lock定义为:

    class scoped_lock
    {
          mutex & m_mutex;
      public:
          scoped_lock(mutex & m) : m_mutex(m) 
          {
              m_mutex.lock();
          }
          ~scoped_lock()
          {
              m_mutex.unlock();
          }
    };
    

    现在你可以使用它们了。

    【讨论】:

    • boost::scoped_lock (mutex) 非常适合这个。离开该范围时,锁应该会自动释放。
    • 在 Boost 库之外有什么可用的吗?
    • @JonahBishop:是的。如果您的编译器支持 C++11 并已实现,您可以使用 std::unique_lock。或者,您可以自己实现它。这是非常琐碎的任务。
    • 很棒的答案。感谢您对范围锁片的澄清!
    • @JonahBishop:我添加了一个实现。
    【解决方案2】:

    鉴于该函数显然不是面向性能的,因此在写入流之前添加一个简单的互斥锁和锁。

    当然,出于安全考虑,锁应该是自动释放的,所以一定要使用 RAII。

    我建议看看 Boost.Threads 库。

    【讨论】:

      【解决方案3】:

      是的,您应该为您的代码添加某种保护。您不需要任何异国情调的东西,您想要访问资源(您的流),而它可能正在被其他东西使用。两种类型的同步对象在此任务中表现良好:Critical Sections 和 Mutex。有关锁的更多详细信息,您可以开始阅读Wikipedia 上的这篇文章。 Mutex 通常速度较慢,但​​可以跨进程共享,这不是您的情况,因此您可以使用简单关键部分来同步您的线程。

      如果您不打算使用第 3 部分库(如出色的 Boost),请提供一些提示。

      EnterCriticalSection函数获取锁。如果资源被其他人锁定,您的线程将被暂停并在资源被其所有者释放时重新激活(此外,您锁定的线程可能会提高其优先级)。 对于短暂的锁,这可能不是最佳解决方案,因为挂起/恢复线程会耗费时间和资源。出于这个原因,你可以设置一个 spin;在挂起您的线程之前,操作系统将花费一点时间在该线程上什么都不做,它可能会给锁定所有者时间来完成其工作并释放线程。要使用它,您必须使用 InitializeCriticalSectionAndSpinCount 而不是 InitializeCriticalSection 初始化您的关键部分。

      如果您打算使用关键部分,您可能会考虑将所有需要的内容包装在一个类中,您将使用变量范围来完成所有操作,并且您的代码会更加清晰(这只是一个示例,不能真正实现太天真了):

      class critical_section
      {
      public:
       critical_section()
       {
        // Here you may use InitializeCriticalSectionAndSpinCount
        InitializeCriticalSection(&_cs);
      
        // You may not need this behavior, anyway here when you create
        // the object you acquire the lock too
        EnterCriticalSection(&_cs);
       }
      
       ~critical_section()
       {
         LeaveCriticalSection(&_cs);
         DeleteCriticalSection(&cs);
       }
      
      private:
       CRITICAL_SECTION _cs;
      };
      

      在 Unix/Linux 下(或者你想要便携)你应该使用 pthread.h 函数来实现 Mutex。

      锁可能不够

      这只是第一步,如果您的应用程序记录大量日志,您可能会减慢等待日志的所有线程(不要做任何事先验,检查和分析)。如果是这种情况,您应该创建一个队列,您的logText() 函数只需在队列中推送一个新的(预先格式化的)项目并调用PulseEvent 来发出事件信号(使用CreateEvent 创建)。您将有第二个线程使用WaitForSingleObject 等待该事件,您的线程将唤醒并从队列中弹出一个项目。您可能仍然需要一些锁定机制(或者您可以编写自己的 non-blocking concurrent queue 以避免任何锁定。此解决方案更快,并且不使用任何类型的锁定,但我认为只有在您想要的情况下才应该这样做研究主题(线程),通常对于简单的日志要求,您不需要添加如此复杂的内容。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-02-23
        • 2020-12-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多