是否为记录器创建单独的线程?

时间:2016-08-02 15:55:24

标签: c++ multithreading qt logging

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

  1. 每个帖子都会自行记录(这涉及互斥,所以可能是最糟糕的方法,但我不确定)
  2. 有一个专用的记录器线程和其他线程直接发布事件(可能比3更快。)
  3. 与2.相同但是消息是通过信号/插槽系统发送的(事实上,这也会导致发布事件)。
  4. 哪一个更好,一般的最佳做法是什么?

    在评论中提出问题后要澄清一些事项:

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

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

    • 这就是问题的本质。我知道最好的答案是“测量!”,但目前应用程序处于早期开发阶段,没有太多可衡量的。从一开始就选择合适的设计总是好的。
    • 在我的情况下,线程可能是一个好主意:它是一个媒体播放器,所以有GUI线程,回放线程,DB /媒体库线程,网络线程池......换句话说,整个动物园的线程。

3 个答案:

答案 0 :(得分:8)

这些是一般性评论,因为我没有使用Qt的经验。关于排队的成本,一般来说:I / O通常会让其他运行时成本变得苍白,所以无所谓。

专用日志记录线程的属性:

  • 好:对程序的运行时行为影响最小。
  • 好:保证单个日志消息(不是来自多个线程的混合输出)。
  • 错误:重要的实施工作。
  • 错误:启动和实际执行日志记录的时间是分离的(这就是重点!)。您可能会看到日志记录输出不在您期望的位置。
  • 错误:终止程序可能吞下最后和最重要的日志消息(Andreas'指向),因此您可能希望添加同步日志功能(这是上述要点的极端情况)。

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

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

答案 1 :(得分: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);
}

答案 2 :(得分:1)

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

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

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<char>的事件。然后,使用单个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至少会执行两次分配。