C++ 为记录器创建单独的线程是否正常?

C++ 为记录器创建单独的线程是否正常?,c++,multithreading,qt,logging,C++,Multithreading,Qt,Logging,我正在用Qt编写一个多线程应用程序(几个线程有自己的事件循环)。记录日志时,我希望日志记录器在日志中包含线程id(它们有有有意义的名称)。Qt默认记录器似乎无法做到这一点。 所以我有三个选择: 每个线程都会自己记录日志(这涉及互斥,所以可能是最糟糕的方法,但我不确定) 有一个专用的记录器线程和其他线程直接将事件发布到日志中(可能比3快) 与2相同。但是消息是通过信号/插槽系统发送的(事实上,这也会导致发布事件) 一般来说,哪一个更好?最佳实践是什么 在评论中提出问题后需要澄清的一些事情: Q

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

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


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

    • QThread有一个标准方法
      postEvent()
      ,它是线程安全的
    所以问题变成了,记录器线程是否需要为每个事件做足够的工作,以证明跨某种队列封送事件数据的成本是合理的

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

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

    专用日志线程的属性:

    • 好:对程序运行时行为的影响最小
    • 好:保证单日志消息(不是来自多个线程的混合输出)
    • 坏:大量的实施工作
    • 糟糕:启动和实际执行日志记录的时间是不耦合的(这就是关键!)。您可能会看到日志输出,而不是预期的结果
    • 坏:终止程序可能会吞掉最后一条也是最重要的日志消息(Andreas的观点),因此您可能需要添加一个同步日志函数(这是上述观点的一个极端)
    直接从每个线程记录日志的(dis)优点与上述相反。没有两个线程可以同时记录日志(要么是因为像
    printf()
    这样的函数隐式地锁定了
    文件,要么是因为您显式地同步了日志函数);这会阻止所有要记录日志的线程,直到当前线程完成。如果日志记录是为了调试目的而进行的,则可能希望不缓冲地进行日志记录(以便在随后的崩溃事件中不会丢失数据),这会加剧运行时影响


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

    我已经使用

    在Qt应用程序中,有一个表示该应用程序的实例

    您可以通过继承、发布和使用应用程序的QApplication对象处理事件来创建自己的事件

    例如,您可能拥有日志事件类

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

    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);
    }
    
    bool主窗口::事件(QEvent*e)
    {
    如果(e->type()==QEvent::User)
    {
    //这是一个日志事件
    MyLogEvent*logEvent=静态转换(e);
    ui.textEdit->appendPlainText(logEvent->logMessage())
    返回true;
    }
    返回QMainWindow::事件(e);
    }
    
    我不太明白为什么每个单独记录日志的线程都需要使用显式互斥锁

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

    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
    的事件。然后,通过单个
    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;
    }
    
    //logger.h
    结构MyLogEvent:QEvent{
    constexpr static QEvent::Type(){return(QEvent::Type)(QEvent::User+1);}
    QVarLengthArray消息;
    MyLogEvent(const char*msg):QEvent(类型()){
    message.append(msg,strlen(msg));
    }
    };
    类记录器:公共QObject{
    ...
    公众:
    静态无效日志(const char*msg){
    QCoreApplication::postEvent(实例(),新的MyLogEvent(消息));
    }
    静态记录器*实例();//singleton必须是线程安全的方法
    };
    //logger.cpp
    ...
    Q_全局_静态(记录器、loggerInstance);
    Logger*Logger::instance(){
    //线程安全,因为QGlobalStatic是。
    返回loggerInstance;
    }
    

    如果您使用了
    QByteArray
    QString
    ,则表达式
    new MyRogeEvent
    将至少执行两次分配。

    您可能还需要对专用日志线程进行同步