【问题标题】:Is creating a separate thread for a logger ok?是否可以为记录器创建单独的线程?
【发布时间】:2016-08-02 15:55:24
【问题描述】:

我正在 Qt 中编写一个多线程应用程序(多个线程具有自己的事件循环)。记录时,我希望记录器在日志中包含一个线程 ID(它们具有有意义的名称)。 Qt 默认记录器似乎无法做到这一点。 所以我有三个选择:

  1. 每个线程都自己进行日志记录(这涉及互斥锁,因此可能是最糟糕的方法,但我不确定)
  2. 有一个专用的记录器线程和其他线程直接将事件发布到其中(可能比 3 更快。)
  3. 同 2。但消息是通过信号/槽系统发送的(事实上,这也会导致发布事件)。

哪一个更好,一般来说最佳做法是什么?


在cmets提问后需要澄清的一些事情:

  • QThread 有一个标准方法postEvent(),即线程安全。

所以问题就变成了,记录器线程是否需要为每个事件做足够的工作来证明跨某种队列编组事件数据的成本是合理的

  • 这就是问题的本质。我知道最好的答案是“测量!”,但目前该应用程序处于早期开发阶段,没有太多要测量的内容。此外,从一开始就选择正确的设计总是好的。
  • 在我的例子中线程可能是个好主意:它是一个媒体播放器,所以有 GUI 线程、播放线程、DB/媒体库线程、网络线程池......换句话说,整个 Zoo 线程。

【问题讨论】:

  • 您可能还需要同步专用日志线程(可能是隐式的,但来自其他线程的事件或帖子在某处排队,必须同步)。
  • 在某种程度上,在所有情况下都需要同步(即互斥锁)。所以问题就变成了,记录器线程是否需要为每个事件做足够的工作来证明跨某种队列编组事件数据的成本是合理的,而不是仅仅在事件源线程上执行工作,并锁定足够长的时间来写入事件数据到公共输出。
  • @JesperJuhl 非并行日志记录改变了运行时行为,可能太多了(例如,当更高的日志级别用于调试某些行为然后消失时)。因为如果没有专用的日志记录线程,您必须在在写入日志条目时的整个时间内阻止其他线程进行日志记录,或者接受杂乱的混合日志,这往往很快就会变得无用。
  • 记住:加入你的线程,如果它们是 pthread 的,记住只有一个参数被传递给你想调用的线程函数,所以如果你想传递的不是线程 ID,就取考虑到这种情况。进程同步,知道是否使用互斥锁,计数信号量和二进制信号量。并且没有互斥量与二进制信号量不同,我让你看看。
  • Qt 默认记录器似乎无法做到这一点。 你不能使用qInstallMessageHandler 来安装自定义日志消息处理程序,并在那里获取“唯一线程 ID”调用者,并将其添加到日志中?

标签: c++ multithreading qt logging


【解决方案1】:

这些都是一般性评论,因为我没有使用 Qt 的经验。关于排队的成本,一般来说:I/O 通常会让其他运行时成本相形见绌,所以应该没关系。

专用日志线程的属性:

  • 良好:对程序运行时行为的影响最小。
  • 好:有保证的单一日志消息(不是来自多个线程的混合输出)。
  • 差:大量的实施工作。
  • 不好:启动和实际执行日志记录的时间是分离的(这就是重点!)。您可能会看到与预期不同的日志输出。
  • 不好:终止程序可能会吞下最后和最重要的日志消息(Andreas 的观点),因此您可能需要添加同步日志功能(这是上述观点的一个极端)。

直接从每个线程进行日志记录的(不利)优势与上述相反。没有两个线程可以同时记录(或者因为像printf() 这样的函数隐式锁定了FILE,或者因为你显式地同步了日志函数);这使得所有想要记录的线程阻塞,直到当前线程完成。如果出于调试目的进行日志记录,则可能希望记录无缓冲(以便在随后发生崩溃时数据不会丢失),这会加剧运行时影响。

这有多糟糕取决于应用程序的性质以及日志记录机制和数据量。

【讨论】:

  • I/O 通常会让其他运行时间成本相形见绌,所以应该没关系 - 这就是我想听到的!所以现在我正在考虑这个解决方案:通常消息在专用线程中处理,但它仍然每次都会锁定一个互斥锁(如果互斥锁解锁,它会非常快)。但是关键消息在调用者的线程中处理,并且由于提到的互斥锁是线程安全的。
【解决方案2】:

我已经使用Qt Event mechanism 以一种非常干净的方式为 Qt 应用程序实现了日志记录机制。

在 Qt 应用程序中,有一个 QApplication 实例代表该应用程序。

您可以通过从QEvent 继承来创建自己的事件,然后使用应用程序的 QApplication 对象发布和处理它们。

所以例如你可能有你的日志事件类

MyLogEvent : public QEvent
{
public:
   MyLogEvent(QString threadId, QString logMessage) : QEvent(QEvent::User)
       { /* Store the ThreadID and log message, with accessor functions */};
}

您可以使用任何 Qt 线程发布事件

MyLogEvent *event = new MyLogEvent(QString("Thread 1"), QString("Something Happened"));
QApplication::postEvent(mainWindow, event);

处理程序可以是主窗口对象(如果您想登录到窗口),也可以是专用对象,例如记录到文件中。

在处理事件的对象中,重写 QObject::event 来处理日志消息

bool MainWindow::event(QEvent *e)
{
   if(e->type()==QEvent::User)
   {
        // This is a log event
        MyLogEvent *logEvent = static_cast<MyLogEvent *>(e);
        ui.textEdit->appendPlainText(logEvent->logMessage())
        return true;
   }
   return QMainWindow::event(e);
}

【讨论】:

    【解决方案3】:

    我不太明白为什么每个单独进行日志记录的线程都需要使用显式互斥锁。

    如果您正在记录到磁盘文件,那么每个线程都可以记录到自己的文件中。您可以使用通用前缀命名文件:

    QFile * logFile(QObject * parent = nullptr) {
      auto baseName = QStringLiteral("MyApplication-");
      auto threadName = QThread::currentThread()->objectName();
      if (threadName.isEmpty())
        return new QTemporaryFile(baseName);
      else
        return new QFile(baseName + threadName);
    }
    

    操作系统正在通过其文件系统互斥锁序列化访问。

    如果您要登录到支持并发访问的数据库,例如选择了适当并发选项的 sqlite,数据库驱动程序将负责序列化访问。

    如果您正在登录到一个公共线程,那么事件队列有一个互斥锁,当您postEvent 时,您会自动使用该互斥锁进行序列化。

    你是对的,使用信号槽机制并没有比直接使用事件更能买到。事实上,它可以保证执行更多的内存分配,因此您应该更喜欢自己发布一个事件,理想情况下是一个使用大小适合“大多数”日志消息的QVarLengthArray&lt;char&gt; 的事件。然后,通过一个 malloc 调用来分配这样的事件:

    // logger.h
    
    struct MyLogEvent : QEvent {
      constexpr static QEvent::Type theType() { return (QEvent::Type)(QEvent::User + 1); }
      QVarLengthArray<char, 128> message;
      MyLogEvent(const char * msg) : QEvent(theType()) {
        message.append(msg, strlen(msg));
      }
    };
    
    class Logger : public QObject {
      ...
    public:
      static void log(const char * msg) {
        QCoreApplication::postEvent(instance(), new MyLogEvent(msg));
      }
      static Logger * instance(); // singleton, must be a thread safe method
    };
    
    // logger.cpp
    ...
    Q_GLOBAL_STATIC(Logger, loggerInstance);
    
    Logger * Logger::instance() {
      // Thread-safe since QGlobalStatic is.
      return loggerInstance;
    }
    

    如果您使用QByteArrayQString,表达式new MyLogEvent 将执行至少两次分配。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-28
      • 1970-01-01
      • 2010-11-20
      • 2018-10-16
      相关资源
      最近更新 更多