C++ 为记录器创建单独的线程是否正常?
我正在用Qt编写一个多线程应用程序(几个线程有自己的事件循环)。记录日志时,我希望日志记录器在日志中包含线程id(它们有有有意义的名称)。Qt默认记录器似乎无法做到这一点。 所以我有三个选择:C++ 为记录器创建单独的线程是否正常?,c++,multithreading,qt,logging,C++,Multithreading,Qt,Logging,我正在用Qt编写一个多线程应用程序(几个线程有自己的事件循环)。记录日志时,我希望日志记录器在日志中包含线程id(它们有有有意义的名称)。Qt默认记录器似乎无法做到这一点。 所以我有三个选择: 每个线程都会自己记录日志(这涉及互斥,所以可能是最糟糕的方法,但我不确定) 有一个专用的记录器线程和其他线程直接将事件发布到日志中(可能比3快) 与2相同。但是消息是通过信号/插槽系统发送的(事实上,这也会导致发布事件) 一般来说,哪一个更好?最佳实践是什么 在评论中提出问题后需要澄清的一些事情: Q
在评论中提出问题后需要澄清的一些事情:
- QThread有一个标准方法
,它是线程安全的postEvent()
- 这就是问题的实质。我知道最好的答案是“测量!”,但目前该应用程序处于早期开发阶段,没有太多可测量的内容。而且,从一开始就选择正确的设计总是好的
- 在我的例子中,线程可能是个好主意:它是一个媒体播放器,所以有GUI线程、回放线程、DB/媒体库线程、网络线程池。。。换言之,就是整个线程动物园
- 好:对程序运行时行为的影响最小
- 好:保证单日志消息(不是来自多个线程的混合输出)
- 坏:大量的实施工作
- 糟糕:启动和实际执行日志记录的时间是不耦合的(这就是关键!)。您可能会看到日志输出,而不是预期的结果
- 坏:终止程序可能会吞掉最后一条也是最重要的日志消息(Andreas的观点),因此您可能需要添加一个同步日志函数(这是上述观点的一个极端)
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
将至少执行两次分配。您可能还需要对专用日志线程进行同步