qDebug() 是线程安全的吗?

25

qDebug()是线程安全的吗?我所说的线程安全不仅仅是指不会崩溃,还包括如果我从不同的线程调用qDebug(),是否可能导致输出混乱?我使用了这段代码进行测试,似乎并不是这样,但是我无法在文档中找到相关信息。

这是我的测试代码:

#include <QtConcurrent>
#include <QApplication>
void print_a() {
    for (int ii = 0; ii < 10000; ii++) {
        qDebug("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
    }
}
void print_b()
{
    for (int ii = 0; ii < 10000; ii++) {
        qDebug("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
    }
}
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QtConcurrent::run(print_a);
    QtConcurrent::run(print_b);
    return a.exec();
}

无论在哪里都没有“a”和“b”混在同一行,但我仍然不确定它是否100%线程安全...


2
文档中说如果一个函数没有标记为线程安全或可重入,那么它不应该从不同的线程中使用。在qDebug()的情况下,它没有说明它是线程安全的,因此很可能不能够从不同的线程中使用。 - thuga
@thuga,那就是我问题的一个有效答案,你应该发表一下 :) - sashoalm
我把我的评论发布为答案。 - thuga
7个回答

16

等等,我再仔细想了想,线程安全是关于使用同一对象的,对吧?但是 qDebug() << "foo"; 构造了一个新的 QDebug 对象,所以我们实际上应该谈论的是可重入性,而不是线程安全。抱歉我之前没有提到,但我现在明白了。 - sashoalm
是的,但QRectQPoint也不是。我发布了一个关于可重入性问题的新问题,您可以在https://dev59.com/tGEh5IYBdhLWcg3wVCLK上查看它。 - sashoalm

16

以下是我的回答和评论:

  1. 如果qDebug()的文档没有提到它是否线程安全,我们应该假定它不是线程安全的。答案很可能取决于平台:qDebug()在系统级别上的实现方式(Linux、Windows等)。

  2. 与线程安全性更广泛的问题不同,我认为你问的更具体的问题是:“在多线程应用程序中使用qDebug()会导致交织的输出行吗?”答案是“有时会”,正如@dmcontador所示的结果所示。当要打印的字符串越来越长时,概率增加,这是由@quetzalcoatl解释的。

  3. 答案不取决于您是使用qDebug("...")还是qDebug() << "...",因为两者最终都会调用系统级别的实现代码。

  4. 对于您的原始示例代码,我很难产生交错的输出行。因此,我创建了一个新的示例,如下所示:

  5. #include <QCoreApplication>
    #include <QtConcurrent>
    
    #define MAX_ITERS 10
    #define MAX_LEN   10000
    
    void print_a()
    {
        QString a(MAX_LEN, 'a');
    
        for(int i = 0; i < MAX_ITERS; ++i) {
            qDebug().noquote() << a;
        }
    }
    
    void print_b()
    {
        QString b(MAX_LEN, 'b');
    
        for(int i = 0; i < MAX_ITERS; ++i) {
            qDebug().noquote() << b;
        }
    }
    
    int main(int argc, char * argv[])
    {
        QCoreApplication a(argc, argv);
        QtConcurrent::run(print_a);
        QtConcurrent::run(print_b);
        return 0;
    }
    

当您增加MAX_LEN时,概率会增加。

  1. 一个跟进的问题是:“如何使用qDebug()产生非交错的输出行?”一个解决方案是在每个qDebug()行上使用QMutex。请注意,我没有尝试过这种不切实际的解决方案。

1
qDebug和QDebug本身都是线程安全的。您看到的“交错输出”源于Qt在大多数配置上默认记录到stderr(标准错误)而不是缓冲区的事实。如果您有这样长的行,请改用其他记录接口(如Linux上的syslogd)。您可以通过将qDebug()替换为fprintf(stderr,...)调用来轻松检查此内容。 - kkoehne

8

很抱歉,它不是线程安全的。另外,我尝试了你的代码,结果输出混乱。

aaaaaaaaaaaabbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbabbbbbbbbbbbbbbbbbb

我在使用Qt5.2.1和mingw48_32编译器时,也成功地运用了qDebug() << "..."


因为我可以在Qt 5.3.1(MinGW 4.8.2 32位)中确认这一点,所以我点了赞。 - mozzbozz

5
事实上,QtDebug相关函数(如qDebug()、qWarning()等)所做的是调用消息处理程序,可以通过调用qInstallMessageHandler()来设置。因此,这取决于您——这个消息处理程序是否线程安全。有一个默认实现,它只将消息打印到stderr,它不能防止不同线程的混合输出,因此,如果您想始终获得来自不同线程的调试消息、警告和错误的非混合逐行输出,您应该像这样使用某种锁定(例如QMutex)安装自己的处理程序:
QMutex messageMutex;

void myMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
    QMutexLocker locker(&messageMutex);

    [print msg and context somewhere]
}

int main(int argc, char **argv)
{
    qInstallMessageHandler(myMessageHandler);

    QApplication app(argc, argv);

    [...]
}

注意: 正如 Kuba Ober 正确指出的,这应该谨慎使用(就像一般的锁定一样)。例如,如果在输出调试信息时从I/O库内部调用QtDebug函数,并且同时使用同一I/O库,则可能会导致死锁(例如,当QtDebug消息处理程序在持有非递归互斥锁的情况下调用I/O时,底层I/O机制调用了某个回调函数,然后此回调函数调用QtDebug函数,该函数再次调用相同的处理程序)。


需要注意的是,这可能是一个可怕的想法。为什么?因为如果在持有其他互斥锁的情况下调用消息处理程序,可能会出现死锁,因为不能保证互斥锁将按照防止死锁所必需的顺序获取。例如,如果标准输出在代码中的任何其他地方使用,C库可能会持有自己的互斥锁等。需要非常小心地确保此代码不会死锁。这是可能的,但不要认为你可以这样做,它就会“正常工作”。 - Kuba hasn't forgotten Monica
如果在使用相同的I/O库来打印调试消息的同时,从某些I/O库内部调用QDebug函数,则可能会发生死锁。因此,应该谨慎使用此功能。我已在我的答案中添加了相应的注释。 - Oleg Derevenetz

2

那个回答是从2006年的,虽然如此。鉴于我的测试代码运行正常而没有混合输出,事情可能已经改变了。我可能需要查看源代码。 - sashoalm
4
注意,输出流可能会被缓冲。缓冲区可能会被内部同步(并且通常是这样的!),但整个机制不一定如此。尝试使用非常大的字符串进行相同的操作,以便缓冲区需要增长或分块字符串并等待。我不知道任何缓冲区可能有多大,但是您当前的测试字符串非常短。 - quetzalcoatl
1
这是关于 QDebug 的问题,你可以通过 qDebug() 而不是 qDebug(..) 来获取它。因此,如果你使用 qDebug() << "aaaaaaaaaaaaaaa" 是不安全的。它并没有告诉我们关于 C 风格版本的任何信息。 - BeniBela
@BeniBela 实际上,qDebug()是一个创建全新QDebug实例的函数。因此,除非涉及全局变量,否则线程不会访问相同的数据 - sashoalm

2

实际上,qDebug( ..text.. ) 在编译时使用gcc是线程安全的。

如果您查看qt(4)源文件qglobal.cppqDebug 调用 qt_message_output,后者调用fprintf(stderr, ...),在glibc中是线程安全的。

qDebug() << .. 是另一回事)


为什么qDebug()会是另一回事呢?它不也调用了fprintf吗? - sashoalm
我也在Stack Overflow上发布了一个关于MSVCRT fprintf的问题,因为我在Windows上使用Qt。 - sashoalm
1
看起来 fprintf() 在 MSVCRT 中也是线程安全的,因此在使用 Visual Studio 工具链的 Qt 中,qDebug() 仍然应该是线程安全的。 - sashoalm
这是 不正确的。请参考此答案:https://dev59.com/nWEh5IYBdhLWcg3wVCPK#23517726 - mozzbozz
@mozzbozz:也许在qt5中已经改变了?顺便说一下,源代码中的#include <QtConcurrentRun>是正确的。在qt4中不存在QtConcurrent - BeniBela
@BeniBela:那可能是不同答案的原因。然而,有人可能会说“千万别依赖于文档未保证的功能”......好吧,关于 #include <QtConcurrentRun> 的问题,我在编辑帖子时没有在Google上找到任何相关信息...嗯,文档说,QtConcurrent 至少可用自Qt 4.4以来。所以我想这是更好的测试代码,因为它兼容所有“当前”的Qt版本(我猜现在没有人再使用<Qt 4.4了?oO) - mozzbozz

1

两者都

qDebug("xx")

以及

qDebug() << "xx"

qInfo,qWarning,qCritical以及像qCDebug,qCInfo,qCWarning,qCritical这样的分类版本都可以安全地同时从不同的线程中使用。
但是,您必须确保日志接收器也可以原子方式处理大数据。这就是混淆的来源,因为stderr显然会断开太长的行。您可以通过在示例中将qDebug()替换为fprintf(stderr)来轻松验证此操作:对我来说,它显示了完全相同的行为。
您可以尝试其他日志记录接收器,例如journald。无论如何,它们可能也会对最大长度施加限制。总的来说,我建议将日志消息的最大长度保持合理。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接