替代Logton类的Singleton

时间:2018-02-01 14:42:23

标签: c++ linux qt logging

我正在设计一个共享库,主要为客户端应用程序提供具有特定日志记录功能的类。我宁愿不假设这个类如何被用来更通用。

cells = document.querySelectorAll('div'); [].forEach.call(cells, function (el) { //console.log(el.nodeName) if (el.hasChildNodes() && el.firstChild.nodeName=="A") { console.log(el)}; });类的职责是使用标题和尾部封装应用程序数据,并将一行写入单个文件,其中文件路径由应用程序指定但不是它的名字。

<div class="box"><a href="#"><img src="#" /></a></div> <div class="box"><img src="#" /></div> <div class="box"><img src="#" /></div>类的第一个(简化)骨架可以是:

Logger

由于应用程序可以在任何地方和任何线程中使用此Logger类,我想确保应用程序只设置class Logger { public: Logger(const QString &filepath); bool log(const QString &datas); ... }; 一次,并且保护并发文件的线程安全访问。

Singleton模式似乎是这个课程的一个很好的候选人,但我问自己3个问题:

如何在不使用C ++ 11或更新的编译器的情况下使其成为线程安全的

第二点是应用程序只应设置Logger一次。使用Singleton,我们仍然可以在filepath中添加filepath,但即使这样可以确保filepath不会更改,也可以在应用程序中使用不同的参数调用

getInstance(const &QString filepath)

这会奏效。但是从应用程序的角度来看,我发现这一点并不清楚,因为开发人员可能会认为它在filepath// let's say this is the firstcall so filepath = filepath1 and won't change Logger::getInstance(filepath1); ... Logger::getInstance(filepath2); // Ok we still have filepath = filepath1 ... // and so on from anywhere in the code and possibly from different threads 中创建了一个日志,但事实并非如此。而且,每次客户端必须调用单例时,他必须传递所有参数。理想情况下,filepath1只应在客户端代码中设置一次,然后只需调用filepath2方法。如何使客户端应用程序更清晰?

最后一点是关于单例模式本身:我宁愿避免它,特别是在多线程上下文中,因为良好的线程安全实现并不像它看起来那么容易。我错过了一些明显的东西吗?

谢谢。

2 个答案:

答案 0 :(得分:2)

由于您正在使用Qt,因此可以通过以下方式解决多线程问题:

class Logger
{
public:
     bool log(const QString &datas)
     {
         QMutexLocker lock(&Logger::mutex);
         //log ...
     }

private:
    static QMutex mutex;
};

所以你可以这样做:

Logger().log("some data");

关于路径,您可以使用静态私有QString来保存它

private:
    static QString path;

以及设置其值的方法

 bool setPath(QString path)
 {
     QMutexLocker lock(&Logger::mutex);
     if(Logger::path.isEmpty())
     {
        Logger::path = path;
        return true;
     }
     return false;
 }

该方法具有bool返回类型,只是为了通知路径是否已经设置,并且可以是静态的来表示其应用程序范围的性质。

使用配置文件的更好解决方案:

     bool log(const QString &datas)
     {
         QMutexLocker lock(&Logger::mutex);
         if(Logger::path.isEmpty())
              loadPathFromConfiguration();
         //log ...
     }

更复杂的模式:

class Logger
{
public:
     bool log(const QString &datas)
     {
         QMutexLocker lock(&Logger::mutex);
         if(!path().isEmpty())
         {
             //log ...
         }
     }

     virtual QString path() const = 0;

private:
    static QMutex mutex;
};

这样,用户被迫提供自己的实现,并且不可能认为可以有多个日志文件。

答案 1 :(得分:1)

首先,为了防止静态顺序初始化失败的后果,你应该完全控制对象何时生效。记录器应该在main中创建;也许是在从组件构建应用程序但是从main实例化的工厂中。 dependency injection框架可以帮助解决这个问题。然后记录器是单例,但它的实例化点是明确控制的。您可以避免任何线程初始化问题,然后:

int main(int argc, char ** argv) {
  QApplication app(argc, argv);
  app.setApplicationName(...);
  // set other application identification data
  Logger logger; // e.g. reads log location from QSettings
  ...
}

此时,您可以使用Logger方法,也可以通过安装消息处理程序来利用全局qDebug/qWarning/qInfo系统。

线程安全发送消息的一种方法是通过线程安全的信号槽机制:

// interface

class Logger : public QObject {
  Q_OBJECT
  static QReadWriteLock m_lock{QReadWriteLock::Recursive};
  static Logger * m_instance;
  static QThread * m_initialThread;
  QFile m_file;
  int m_flushInterval = 25, m_flushCountdown = {};
  // this function is thread-safe
  static void log(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
     QString output;
     /* format the message into output here */
     QReadLock lock(&m_lock);
     if (m_instance) emit m_instance->log_req(output);
  }
  Q_SIGNAL void log_req(const QString &);
  Q_SIGNAL void finish_req();
  void log_ind(const QString & entry) {
    m_file.write(entry.toLocal8Bit());
    m_file.write("\r\n");
    if (--m_flushCountdown <= 0) {
      m_file.flush();
      m_flushCountdown = m_flushInterval;
    }
  }
  void finish_ind() {
    QReadLock lock(&m_lock);
    moveToThread(m_initialThread);
  }
public:
  Logger(const QString & path, QObject * parent = {}) : QObject(parent) {
    m_file.setFileName(path);
    // etc.
    connect(this, &Logger::log_req, this, &Logger::log_ind);
    connect(this, &Logger::finish_req, this, &Logger::finish_ind, Qt::BlockingQueuedConnection);
    QWriteLock lock(&m_lock);
    Q_ASSERT(!m_instance);
    m_instance = this;
    m_initialThread = QThread::currentThread();
    m_handler = qInstallMessageHandler(&Logger::log);
  }
  ~Logger() {
    Q_ASSERT(currentThread() == m_initialThread);
    if (thread() != m_initialThread)
      finish_req(); // move ourselves back to the instantiating thread
    QWriteLock lock(&m_lock);
    qInstallMessageHandler(m_handler);
    m_instance = {};
  }
  static void log(const QString & output) {
     QReadLock lock(&m_lock);
     if (m_instance) emit m_instance->log_req(output);
  }
};

// implementation

QReadWriteLock Logger::m_lock;
Logger * Logger::m_instance;
QThread * Logger::m_initialThread;

以上还需要一些对错误做出反应的方法。没有可用的日志输出的操作是否可接受? - 对错误的反应将取决于此。

然后Qt会自动为您处理线程安全问题,您自己必须处理的唯一事情是对实例指针和其他静态成员的线程安全访问。具体来说,你必须避免测试和使用之间的竞争,即以下是racy和usafe:if (m_instance) m_instance->method();

然后将记录器放入自己的专用线程中很简单:

class SafeThread : public Thread {
public:
  ~SafeThread() { quit(); wait(); }
};

int main(int argc, char ** argv) {
  ...
  SafeThread loggerThread;
  loggerThread.start();

  Logger logger;
  logger.moveToThread(&loggerThread);
  ... 
}

首先销毁记录器实例。析构函数将请求实例将其自身从其线程移回到实例化的线程,然后继续销毁。 SafeThreadQThread的RAII模型,也会安全地破坏自己。